#include "adapters/eft/aimbot/AimbotTargetScanner.h"

#include <cstddef>
#include <limits>
#include <string>

#include "adapters/eft/aimbot/AimbotDiagnostics.h"
#include "adapters/eft/common/ManagedObjectAccess.h"
#include "adapters/eft/common/PlayerWorldReader.h"
#include "adapters/unity/camera/UnityCameraFrameSource.h"

namespace ta::adapters::eft::aimbot
{
    namespace
    {
        constexpr std::size_t kMaxPlayersToScan = 128;
    }

    std::string TargetScanner::read_nickname(ta::adapters::il2cpp::Il2CppObjectInstance player)
    {
        if (!ta::adapters::eft::common::is_usable_object(player.get_instance()))
        {
            diag::warn_once("nickname_null_player", "read_nickname called with null player");
            return {};
        }

        std::string nickname = ta::adapters::eft::common::get_player_nickname(player);
        if (nickname.empty())
            diag::warn_once("nickname_empty", "Player nickname could not be resolved");
        return nickname;
    }

    bool TargetScanner::try_get_head_position(Il2CppObject* player_object, ta::adapters::unity::vector3& out_head_position)
    {
        if (!ta::adapters::eft::common::is_usable_object(player_object)) return false;

        Il2CppObject* body_object = ta::adapters::eft::common::get_object_method(
            player_object,
            "get_PlayerBody");
        if (!ta::adapters::eft::common::is_usable_object(body_object))
        {
            diag::warn_once("head_no_body", "Unable to resolve player body");
            return false;
        }

        ta::adapters::il2cpp::Il2CppObjectInstance player_body(body_object);
        Il2CppObject* bones_object = nullptr;
        if (!player_body.try_get_field("PlayerBones", bones_object) ||
            !ta::adapters::eft::common::is_usable_object(bones_object))
        {
            diag::warn_once("head_no_player_bones", "PlayerBody.PlayerBones was not found");
            return false;
        }

        ta::adapters::il2cpp::Il2CppObjectInstance player_bones(bones_object);
        Il2CppObject* head_object = nullptr;
        if (!player_bones.try_get_field("Head", head_object) ||
            !ta::adapters::eft::common::is_usable_object(head_object))
        {
            diag::warn_once("head_no_head", "PlayerBones.Head was not found");
            return false;
        }

        ta::adapters::il2cpp::Il2CppObjectInstance head(head_object);
        const auto get_position = head.get_method<ta::adapters::unity::vector3(*)(Il2CppObject*)>("get_position", 0);
        if (!get_position)
        {
            diag::warn_once("head_no_position", "Head.get_position was not found");
            return false;
        }

        out_head_position = get_position(head.get_instance());
        return true;
    }

    ScanResult TargetScanner::scan_target(ta::adapters::il2cpp::Il2CppObjectInstance game_world_instance,
                                          ta::adapters::il2cpp::Il2CppObjectInstance main_player,
                                          ta::adapters::unity::vector3 screen_center)
    {
        ScanResult result{};
        if (!game_world_instance.get_instance() || !ta::adapters::eft::common::is_usable_object(main_player.get_instance()))
        {
            diag::warn_once("scan_invalid_context", "scan_target called with invalid game world or main player");
            return result;
        }

        result.local_nickname = read_nickname(main_player);
        if (result.local_nickname.empty())
        {
            diag::warn_once("scan_empty_local_name", "scan_target could not resolve local nickname");
            return result;
        }

        Il2CppObject* camera = ta::adapters::unity::graphics::UnityCameraFrameSource::current();
        if (!ta::adapters::eft::common::is_usable_object(camera))
        {
            diag::warn_once("scan_no_camera", "scan_target could not resolve current camera");
            return result;
        }

        if (!ta::adapters::eft::common::alive_players_list(game_world_instance))
        {
            diag::warn_once("scan_no_alive_list", "GameWorld.AllAlivePlayersList was not found");
            return result;
        }

        float closest = (std::numeric_limits<float>::max)();
        ta::adapters::unity::vector3 closest_pos{};
        std::size_t players_seen = 0;
        std::size_t players_named = 0;
        std::size_t heads_resolved = 0;
        std::size_t visible_heads = 0;

        ta::adapters::eft::common::for_each_alive_player(
            game_world_instance,
            result.local_nickname,
            [&](const ta::adapters::eft::common::WorldPlayerEntry& player, int)
            {
                ++players_seen;

                if (player.nickname.empty()) return;
                ++players_named;
                if (player.is_local) return;

                ta::adapters::unity::vector3 head_pos{};
                if (!try_get_head_position(player.object, head_pos)) return;
                ++heads_resolved;

                ta::adapters::unity::vector3 screen_pos{};
                if (!ta::adapters::unity::graphics::UnityCameraFrameSource::project_visible(camera, head_pos, screen_pos)) return;
                ++visible_heads;

                screen_pos.z = 0;
                const float dist = screen_pos.distance(screen_center);
                if (dist < closest)
                {
                    closest = dist;
                    closest_pos = head_pos;
                    result.has_target = true;
                }
            },
            kMaxPlayersToScan);

        if (players_seen == 0)
            diag::info_every("scan_empty_alive_list", 120, "Alive player list is empty");

        if (result.has_target)
            result.target = closest_pos;

        diag::info_every("scan_summary", 120,
                         "scan summary: seen=" + std::to_string(players_seen) +
                         " named=" + std::to_string(players_named) +
                         " heads=" + std::to_string(heads_resolved) +
                         " visible=" + std::to_string(visible_heads) +
                         " has_target=" + std::to_string(result.has_target));

        return result;
    }
}
