#pragma once

#include <Windows.h>
#include <cstddef>
#include <cmath>
#include <functional>
#include <string>

#include "adapters/il2cpp/Il2CppObjectInstance.h"
#include "adapters/il2cpp/Il2CppResolver.h"
#include "adapters/il2cpp/Il2CppObjectAccess.h"
#include "adapters/il2cpp/Il2CppString.h"
#include "external/il2cpp/il2cpp-api-types.h"
#include "external/il2cpp/il2cpp-object-internals.h"

#include "adapters/unity/bindings/UnityTypes.h"

namespace ta::adapters::unity
{
    using find_objects_of_type_sig = Il2CppArray*(*)(PVOID);
    inline find_objects_of_type_sig find_objects_of_type_internal = nullptr;
    using get_name_sig = Il2CppString*(*)(Il2CppObject*);
    inline get_name_sig get_name_internal = nullptr;
    using get_component_sig = Il2CppObject*(*)(Il2CppObject*, Il2CppObject*);
    inline get_component_sig get_component_internal = nullptr;
    using get_position_sig = vector3(*)(Il2CppObject*);
    inline get_position_sig get_position_internal = nullptr;
    using set_position_sig = void(*)(Il2CppObject*, vector3);
    inline set_position_sig set_position_internal = nullptr;
    using component_get_game_object_sig = Il2CppObject*(*)(Il2CppObject*);
    inline component_get_game_object_sig component_get_game_object_internal = nullptr;
    using world_to_screen_point_sig = vector3(*)(Il2CppObject*, vector3);
    inline world_to_screen_point_sig world_to_screen_point_internal = nullptr;
    using camera_get_current_sig = Il2CppObject*(*)();
    inline camera_get_current_sig camera_get_current_internal = nullptr;
    using camera_get_dimension_sig = int(*)(Il2CppObject*);
    inline camera_get_dimension_sig camera_get_pixel_width_internal = nullptr;
    inline camera_get_dimension_sig camera_get_pixel_height_internal = nullptr;
    using camera_get_matrix_sig = matrix4x4(*)(Il2CppObject*);
    inline camera_get_matrix_sig camera_get_world_to_camera_matrix_internal = nullptr;
    inline camera_get_matrix_sig camera_get_projection_matrix_internal = nullptr;

    inline const Il2CppClass* camera = nullptr;
    inline Il2CppObject* transform_type = nullptr;

    inline void reset_api() noexcept
    {
        find_objects_of_type_internal = nullptr;
        get_name_internal = nullptr;
        get_component_internal = nullptr;
        get_position_internal = nullptr;
        set_position_internal = nullptr;
        component_get_game_object_internal = nullptr;
        world_to_screen_point_internal = nullptr;
        camera_get_current_internal = nullptr;
        camera_get_pixel_width_internal = nullptr;
        camera_get_pixel_height_internal = nullptr;
        camera_get_world_to_camera_matrix_internal = nullptr;
        camera_get_projection_matrix_internal = nullptr;
        camera = nullptr;
        transform_type = nullptr;
    }

    [[nodiscard]] inline bool is_initialized() noexcept
    {
        return find_objects_of_type_internal &&
               get_name_internal &&
               get_component_internal &&
               get_position_internal &&
               set_position_internal &&
               component_get_game_object_internal &&
               world_to_screen_point_internal &&
               camera_get_current_internal &&
               camera_get_pixel_width_internal &&
               camera_get_pixel_height_internal &&
               camera_get_world_to_camera_matrix_internal &&
               camera_get_projection_matrix_internal &&
               transform_type;
    }

    inline bool init()
    {
        reset_api();

        Il2CppImage* unity_engine = ta::adapters::il2cpp::resolve_image("UnityEngine.CoreModule.dll");
        if (!unity_engine) return false;

        const Il2CppClass* object = ta::adapters::il2cpp::resolve_class(unity_engine, "UnityEngine", "Object");
        const Il2CppClass* game_object = ta::adapters::il2cpp::resolve_class(unity_engine, "UnityEngine", "GameObject");
        const Il2CppClass* transform = ta::adapters::il2cpp::resolve_class(unity_engine, "UnityEngine", "Transform");
        const Il2CppClass* component = ta::adapters::il2cpp::resolve_class(unity_engine, "UnityEngine", "Component");
        camera = ta::adapters::il2cpp::resolve_class(unity_engine, "UnityEngine", "Camera");
        if (!object || !game_object || !transform || !component || !camera) return false;

        find_objects_of_type_internal = ta::adapters::il2cpp::get_method<find_objects_of_type_sig>(object, "FindObjectsOfType", 1);
        get_name_internal = ta::adapters::il2cpp::get_method<get_name_sig>(object, "GetName", 1);
        get_component_internal = ta::adapters::il2cpp::get_method<get_component_sig>(game_object, "GetComponent", 1);
        get_position_internal = ta::adapters::il2cpp::get_method<get_position_sig>(transform, "get_position", 0);
        set_position_internal = ta::adapters::il2cpp::get_method<set_position_sig>(transform, "set_position", 1);
        component_get_game_object_internal = ta::adapters::il2cpp::get_method<component_get_game_object_sig>(
            component, "get_gameObject", 0);
        world_to_screen_point_internal = ta::adapters::il2cpp::get_method<world_to_screen_point_sig>(
            camera, "WorldToScreenPoint", 1);
        camera_get_current_internal = ta::adapters::il2cpp::get_method<camera_get_current_sig>(camera, "get_current", 0);
        camera_get_pixel_width_internal = ta::adapters::il2cpp::get_method<camera_get_dimension_sig>(camera, "get_pixelWidth", 0);
        camera_get_pixel_height_internal = ta::adapters::il2cpp::get_method<camera_get_dimension_sig>(camera, "get_pixelHeight", 0);
        camera_get_world_to_camera_matrix_internal = ta::adapters::il2cpp::get_method<camera_get_matrix_sig>(
            camera, "get_worldToCameraMatrix", 0);
        camera_get_projection_matrix_internal = ta::adapters::il2cpp::get_method<camera_get_matrix_sig>(
            camera, "get_projectionMatrix", 0);

        transform_type = ta::adapters::il2cpp::get_system_type(unity_engine, "UnityEngine", "Transform");
        return is_initialized();
    }

    inline Il2CppObject** find_objects_of_type(Il2CppObject* type, _Out_ size_t* amount)
    {
        if (amount) *amount = 0;
        if (!find_objects_of_type_internal || !type || !amount) return nullptr;

        Il2CppArray* array = find_objects_of_type_internal(type);
        if (!array || array->max_length <= 0) return nullptr;

        *amount = static_cast<size_t>(array->max_length);
        return reinterpret_cast<Il2CppObject**>(&array->data);
    }

    inline std::string get_name(Il2CppObject* object)
    {
        if (!get_name_internal || !object) return {};
        Il2CppString* name = get_name_internal(object);
        return ta::adapters::il2cpp::conv_string(name);
    }

    inline vector3 transform_get_pos(Il2CppObject* transform)
    {
        if (!get_position_internal || !transform) return {};
        return get_position_internal(transform);
    }

    inline void transform_set_pos(Il2CppObject* transform, const vector3 position)
    {
        if (set_position_internal && transform) set_position_internal(transform, position);
    }

    inline Il2CppObject* component_get_game_object(Il2CppObject* component)
    {
        if (!component_get_game_object_internal || !component) return nullptr;
        return component_get_game_object_internal(component);
    }

    inline Il2CppObject* gameobject_get_component(Il2CppObject* game_object, Il2CppObject* system_type)
    {
        if (!get_component_internal || !game_object || !system_type) return nullptr;
        return get_component_internal(game_object, system_type);
    }

    inline Il2CppObject* gameobject_get_transform(Il2CppObject* game_object)
    {
        return gameobject_get_component(game_object, transform_type);
    }

    inline vector3 world_to_screen_point(Il2CppObject* camera_inst, vector3 pos)
    {
        if (!world_to_screen_point_internal || !camera_inst) return {};
        return world_to_screen_point_internal(camera_inst, pos);
    }

    inline int camera_pixel_width(Il2CppObject* camera_inst)
    {
        if (!camera_get_pixel_width_internal || !camera_inst) return 0;
        return camera_get_pixel_width_internal(camera_inst);
    }

    inline int camera_pixel_height(Il2CppObject* camera_inst)
    {
        if (!camera_get_pixel_height_internal || !camera_inst) return 0;
        return camera_get_pixel_height_internal(camera_inst);
    }

    inline bool matrix_is_finite(const matrix4x4& matrix) noexcept
    {
        for (const float value : matrix.values)
        {
            if (!std::isfinite(value))
                return false;
        }
        return true;
    }

    inline bool camera_matrices(Il2CppObject* camera_inst, matrix4x4& view, matrix4x4& projection)
    {
        if (!camera_inst || !camera_get_world_to_camera_matrix_internal || !camera_get_projection_matrix_internal)
            return false;

        const matrix4x4 next_view = camera_get_world_to_camera_matrix_internal(camera_inst);
        const matrix4x4 next_projection = camera_get_projection_matrix_internal(camera_inst);
        if (!matrix_is_finite(next_view) || !matrix_is_finite(next_projection))
            return false;

        view = next_view;
        projection = next_projection;
        return true;
    }

    inline Il2CppObject* get_current_camera()
    {
        if (!camera_get_current_internal) return nullptr;
        return camera_get_current_internal();
    }

    inline int transform_get_child_count(Il2CppObject* transform)
    {
        if (!transform || !transform->klass) return 0;
        ta::adapters::il2cpp::Il2CppObjectInstance instance(transform);
        const auto method = instance.get_method<int(*)(Il2CppObject*)>("get_childCount", 0);
        return method ? method(transform) : 0;
    }

    inline Il2CppObject* transform_get_child(Il2CppObject* transform, const int index)
    {
        if (!transform || !transform->klass || index < 0) return nullptr;
        ta::adapters::il2cpp::Il2CppObjectInstance instance(transform);
        const auto method = instance.get_method<Il2CppObject*(*)(Il2CppObject*, int)>("GetChild", 1);
        return method ? method(transform, index) : nullptr;
    }

    inline Il2CppObject* transform_get_game_object(Il2CppObject* transform)
    {
        if (!transform || !transform->klass) return nullptr;
        ta::adapters::il2cpp::Il2CppObjectInstance instance(transform);
        const auto method = instance.get_method<Il2CppObject*(*)(Il2CppObject*)>("get_gameObject", 0);
        return method ? method(transform) : nullptr;
    }

    inline void transform_apply_to_all_children(Il2CppObject* transform, const std::function<void(Il2CppObject*)>& func)
    {
        if (!transform || !func) return;

        const int child_count = transform_get_child_count(transform);
        for (int i = 0; i < child_count; ++i)
        {
            Il2CppObject* child = transform_get_child(transform, i);
            if (!child) continue;
            transform_apply_to_all_children(child, func);
            func(child);
        }
    }
}
