#pragma once

#include "../il2cpp/Il2CppObjectInstance.h"
#include "../il2cpp/il2utils.h"
#include "../il2cpp/unity.h"

// Skeleton line-of-sight via EFT PhysicsExtensions.Linecast (safer than raw Unity Physics.Linecast).
namespace skeleton_vis
{
    inline constexpr int fallback_layer_mask = static_cast<int>(0x80400000u);
    inline constexpr int query_ignore_triggers = 1; // QueryTriggerInteraction.Ignore

    struct layer_mask_field { int m_mask = 0; };

    struct raycast_hit
    {
        unity::vector3 m_point{};
        unity::vector3 m_normal{};
        uint32_t m_face_id = 0;
        float m_distance = 0.f;
        unity::vector2 m_uv{};
        int m_collider_instance_id = 0;
    };

    using linecast_ext_sig = bool(*)(unity::vector3, unity::vector3, int, int, raycast_hit*);
    using hit_get_transform_sig = Il2CppObject*(*)(raycast_hit*);
    using transform_get_parent_sig = Il2CppObject*(*)(Il2CppObject*);

    inline bool g_initialized = false;
    inline linecast_ext_sig g_linecast = nullptr;
    inline hit_get_transform_sig g_hit_get_transform = nullptr;
    inline transform_get_parent_sig g_transform_get_parent = nullptr;

    inline bool get_bifacial_position(Il2CppObject* bifacial, unity::vector3& out)
    {
        if (!bifacial || !bifacial->klass) return false;
        const MethodInfo* method = il2utils::resolve_method(bifacial->klass, "get_position", 0);
        if (!method || !method->methodPointer) return false;
        out = reinterpret_cast<unity::vector3(*)(Il2CppObject*)>(method->methodPointer)(bifacial);
        return true;
    }

    inline int read_static_layer_mask(const Il2CppClass* cls, const char* field_name)
    {
        if (!cls) return 0;
        FieldInfo* field = il2utils::resolve_field(cls, field_name);
        if (!field) return 0;

        layer_mask_field value{};
        il2cpp::il2cpp_field_static_get_value(field, &value);
        return value.m_mask;
    }

    inline int try_get_occlusion_layer_mask()
    {
        static int cached_mask = 0;
        static bool tried = false;
        if (tried) return cached_mask ? cached_mask : fallback_layer_mask;
        tried = true;

        // OLD (containers only — missed static LowPoly walls):
        // int mask = read_efthardsettings("WEAPON_OCCLUSION_LAYERS") || fallback_layer_mask;

        int mask = fallback_layer_mask;

        Il2CppImage* asm_img = il2utils::resolve_image("Assembly-CSharp.dll");
        if (!asm_img)
        {
            cached_mask = mask;
            return cached_mask;
        }

        const Il2CppClass* settings_class = il2utils::resolve_class(asm_img, "EFT", "EFTHardSettings");
        if (settings_class)
        {
            const MethodInfo* get_instance = il2utils::resolve_method(settings_class, "get_Instance", 0);
            if (get_instance && get_instance->methodPointer)
            {
                Il2CppObject* settings = reinterpret_cast<Il2CppObject*(*)()>(get_instance->methodPointer)();
                if (settings)
                {
                    Il2CppObjectInstance settings_inst(settings);
                    mask |= settings_inst.get_field<layer_mask_field>("WEAPON_OCCLUSION_LAYERS").m_mask;
                    mask |= settings_inst.get_field<layer_mask_field>("ENVIRONMENT_HIT_MASK").m_mask;
                    mask |= settings_inst.get_field<layer_mask_field>("BaseMask").m_mask;
                }
            }
        }

        // Static map geometry (LowPoly walls/terrain) + dynamic interactives (containers/loot).
        const Il2CppClass* layers_class = il2utils::resolve_class(asm_img, "", "LayersMaskController");
        if (layers_class)
        {
            mask |= read_static_layer_mask(layers_class, "HighPolyWithTerrainMask");
            mask |= read_static_layer_mask(layers_class, "HighPolyWithTerrainNoGrassMask");
            mask |= read_static_layer_mask(layers_class, "HighPolyCollider");
            mask |= read_static_layer_mask(layers_class, "LowPolyColliderLayerMask");
            mask |= read_static_layer_mask(layers_class, "TerrainMask");
            mask |= read_static_layer_mask(layers_class, "PlayerStaticCollisionsMask");
            mask |= read_static_layer_mask(layers_class, "InteractiveMask");
            mask |= read_static_layer_mask(layers_class, "LootCollisionMask");
        }

        const Il2CppClass* occlusion_class = il2utils::resolve_class(
            asm_img, "Audio.SpatialSystem.Utils", "SoundOcclusionUtils");
        if (occlusion_class)
        {
            const MethodInfo* high_poly = il2utils::resolve_method(
                occlusion_class, "get_HighPolyOcclusionMask", 0);
            const MethodInfo* low_poly = il2utils::resolve_method(
                occlusion_class, "get_LowPolyOcclusionMask", 0);
            if (high_poly && high_poly->methodPointer)
                mask |= reinterpret_cast<int(*)()>(high_poly->methodPointer)();
            if (low_poly && low_poly->methodPointer)
                mask |= reinterpret_cast<int(*)()>(low_poly->methodPointer)();
        }

        cached_mask = mask;
        return cached_mask;
    }

    inline void ensure_init()
    {
        if (g_initialized) return;

        Il2CppImage* asm_img = il2utils::resolve_image("Assembly-CSharp.dll");
        Il2CppImage* physics_image = il2utils::resolve_image("UnityEngine.PhysicsModule.dll");
        Il2CppImage* core_image = il2utils::resolve_image("UnityEngine.CoreModule.dll");
        if (!asm_img || !physics_image || !core_image) return;

        const Il2CppClass* ext_class = il2utils::resolve_class(asm_img, "", "PhysicsExtensions");
        const Il2CppClass* raycast_hit_class = il2utils::resolve_class(physics_image, "UnityEngine", "RaycastHit");
        const Il2CppClass* transform_class = il2utils::resolve_class(core_image, "UnityEngine", "Transform");
        if (!ext_class || !raycast_hit_class || !transform_class) return;

        const MethodInfo* linecast_method = il2utils::resolve_method(
            ext_class, "Linecast", 5);
        const MethodInfo* hit_transform_method = il2utils::resolve_method(
            raycast_hit_class, "get_transform", 0);
        const MethodInfo* parent_method = il2utils::resolve_method(
            transform_class, "get_parent", 0);

        if (!linecast_method || !linecast_method->methodPointer) return;
        if (!hit_transform_method || !hit_transform_method->methodPointer) return;
        if (!parent_method || !parent_method->methodPointer) return;

        g_linecast = reinterpret_cast<linecast_ext_sig>(linecast_method->methodPointer);
        g_hit_get_transform = reinterpret_cast<hit_get_transform_sig>(hit_transform_method->methodPointer);
        g_transform_get_parent = reinterpret_cast<transform_get_parent_sig>(parent_method->methodPointer);
        g_initialized = g_linecast && g_hit_get_transform && g_transform_get_parent;
    }

    inline bool is_ready()
    {
        ensure_init();
        return g_initialized;
    }

    inline Il2CppObject* get_player_transform(Il2CppObject* player_obj)
    {
        if (!player_obj) return nullptr;
        Il2CppObject* player_go = unity::component_get_game_object(player_obj);
        if (!player_go) return nullptr;
        return unity::gameobject_get_transform(player_go);
    }

    inline bool is_player_transform(Il2CppObject* hit_transform, Il2CppObject* player_obj)
    {
        const Il2CppObject* player_tr = get_player_transform(player_obj);
        if (!player_tr || !hit_transform) return false;

        Il2CppObject* current = hit_transform;
        for (int depth = 0; depth < 48 && current; ++depth)
        {
            if (current == player_tr) return true;
            current = g_transform_get_parent(current);
        }
        return false;
    }

    inline bool try_get_fireport_position(Il2CppObject* player_obj, unity::vector3& out)
    {
        if (!player_obj) return false;

        Il2CppObjectInstance player_inst(player_obj);
        const auto get_fireport = player_inst.get_method<Il2CppObject*(*)(Il2CppObject*)>("get_Fireport", 0);
        if (!get_fireport) return false;

        Il2CppObject* fireport = get_fireport(player_obj);
        if (!fireport) return false;

        return get_bifacial_position(fireport, out);
    }

    inline bool try_get_visibility_origin(Il2CppObject* main_player, unity::vector3& out)
    {
        Il2CppObject* cam = unity::get_current_camera();
        if (cam)
        {
            Il2CppObject* cam_go = unity::component_get_game_object(cam);
            Il2CppObject* cam_tr = cam_go ? unity::gameobject_get_transform(cam_go) : nullptr;
            if (cam_tr)
            {
                out = unity::transform_get_pos(cam_tr);
                return true;
            }
        }

        if (try_get_fireport_position(main_player, out))
            return true;

        Il2CppObject* main_tr = get_player_transform(main_player);
        if (!main_tr) return false;

        out = unity::transform_get_pos(main_tr);
        out.y += 1.6f;
        return true;
    }

    // Occlusion check: blocked only when the first hit is NOT part of the target player hierarchy.
    inline bool is_target_visible(const unity::vector3& origin, const unity::vector3& target,
                                  Il2CppObject* target_player_obj)
    {
        if (!is_ready() || !target_player_obj) return true;

        const int layer_mask = try_get_occlusion_layer_mask();
        raycast_hit hit{};

        if (!g_linecast(origin, target, layer_mask, query_ignore_triggers, &hit))
            return true;

        Il2CppObject* hit_transform = g_hit_get_transform(&hit);
        if (!hit_transform) return true;

        return is_player_transform(hit_transform, target_player_obj);
    }
}