#include "adapters/unity/chams/WeaponChamsService.h"

#include <string>
#include <vector>

#include "adapters/il2cpp/Il2CppResolver.h"
#include "adapters/il2cpp/Il2CppObjectAccess.h"
#include "adapters/il2cpp/Il2CppString.h"
#include "adapters/unity/bindings/UnityTypes.h"
#include "adapters/il2cpp/Il2CppCollections.h"
#include "adapters/unity/bindings/UnityApi.h"
#include "adapters/unity/chams/ChamsService.h"
#include "adapters/unity/chams/ChamsSupport.h"
#include "foundation/diagnostics/OnceLogger.h"

namespace ta::adapters::unity::chams
{
WeaponChamsService::Signature WeaponChamsService::make_signature(const ChamsConfigSnapshot& config)
{
    return {config.weapon_type, config.weapon_color,
            config.include_scope, config.player_chams};
}

Il2CppObject* WeaponChamsService::current_weapon_key(ta::adapters::il2cpp::Il2CppObjectInstance main_player)
{
    if (!main_player.get_instance())
        return nullptr;

    using get_hands_controller_sig = Il2CppObject*(*)(Il2CppObject*);
    const auto get_hands_controller = main_player.get_method<get_hands_controller_sig>("get_HandsController", 0);
    if (!get_hands_controller)
    {
        ta::foundation::diagnostics::OnceLogger::error(
            "CHAMS", "weapon-hands-controller-method",
            "Weapon chams failed: get_HandsController unavailable");
        return nullptr;
    }

    Il2CppObject* hands_controller = get_hands_controller(main_player.get_instance());
    if (!hands_controller)
        return nullptr;

    ta::adapters::il2cpp::Il2CppObjectInstance hands_controller_instance(hands_controller);
    using get_item_sig = Il2CppObject*(*)(Il2CppObject*);
    const auto get_item = hands_controller_instance.get_method<get_item_sig>("get_Item", 0);
    if (!get_item)
    {
        ta::foundation::diagnostics::OnceLogger::error(
            "CHAMS", "weapon-item-method",
            "Weapon chams failed: HandsController.get_Item unavailable");
        return nullptr;
    }

    Il2CppObject* item = get_item(hands_controller);
    if (!item)
    {
        ta::foundation::diagnostics::OnceLogger::error(
            "CHAMS", "weapon-item-null",
            "Weapon chams failed: HandsController.get_Item returned null");
        return nullptr;
    }
    return item;
}

void WeaponChamsService::restore_all()
{
    for (auto it = cached_materials_.rbegin(); it != cached_materials_.rend(); ++it)
        (void)ChamsService::restore_material_state(it->original);
    cached_materials_.clear();
    applied_ = false;
}

void WeaponChamsService::rebuild_material_cache(const Il2CppImage* unity_image,
                                                ta::adapters::il2cpp::Il2CppObjectInstance main_player)
{
    restore_all();

    Il2CppObject* renderer_type = ta::adapters::il2cpp::get_system_type(unity_image, "UnityEngine", "Renderer");
    if (!renderer_type)
        return;

    const auto main_player_game_object = ta::adapters::unity::component_get_game_object(main_player.get_instance());
    const auto main_player_transform = ta::adapters::unity::gameobject_get_transform(main_player_game_object);
    if (!main_player_transform)
        return;

    ta::adapters::unity::transform_apply_to_all_children(main_player_transform,
                                           [renderer_type, this](Il2CppObject* transform)
                                           {
                                               Il2CppObject* game_object = ta::adapters::unity::transform_get_game_object(transform);
                                               Il2CppObject* renderer = ta::adapters::unity::gameobject_get_component(game_object, renderer_type);
                                               if (!renderer)
                                                   return;

                                               ta::adapters::il2cpp::Il2CppObjectInstance renderer_instance(renderer);
                                               using get_material_sig = Il2CppObject*(*)(Il2CppObject*);
                                               const auto get_material = renderer_instance.get_method<get_material_sig>("get_material", 0);
                                               if (!get_material)
                                                   return;

                                               Il2CppObject* material = get_material(renderer);
                                               if (!material)
                                                   return;

                                               ChamsMaterialState original{};
                                               if (!ChamsService::capture_material_state(material, original))
                                                   return;

                                               const std::string classification_name =
                                                   ta::adapters::unity::get_name(transform) + " " +
                                                   ta::adapters::unity::get_name(game_object) + " " +
                                                   ta::adapters::unity::get_name(material);
                                               cached_materials_.push_back({
                                                   original,
                                                   ChamsMaterialClassifier::is_scope(classification_name),
                                                   ChamsMaterialClassifier::is_local_player_body(classification_name),
                                                   ChamsMaterialClassifier::is_weapon_gear(classification_name)
                                               });
                                           });
}

bool WeaponChamsService::apply_cached_materials(const Il2CppImage* unity_image,
                                                const ChamsConfigSnapshot& config)
{
    const std::string shader_name = ChamsShaderCatalog::shader_name(config.weapon_type);
    if (shader_name.empty())
        return false;

    bool applied_any = false;
    for (const CachedMaterial& cached : cached_materials_)
    {
        if (!cached.original.material)
            continue;
        if (cached.is_gear)
            continue;
        if (cached.is_scope && !config.include_scope)
            continue;
        if (cached.is_local_player && !config.player_chams)
            continue;

        applied_any |= ChamsService::apply_material_chams(
            unity_image,
            ta::adapters::il2cpp::Il2CppObjectInstance(cached.original.material),
            shader_name.c_str(),
            config.weapon_color,
            config.weapon_color);
    }

    return applied_any;
}

void WeaponChamsService::update(const Il2CppImage* unity_image,
                                ta::adapters::il2cpp::Il2CppObjectInstance main_player,
                                const ChamsConfigSnapshot& config)
{
    if (!unity_image || !main_player.get_instance())
    {
        reset();
        return;
    }

    const Signature signature = make_signature(config);
    Il2CppObject* weapon_key = current_weapon_key(main_player);

    const bool weapon_changed = weapon_key != cached_weapon_key_;
    const bool signature_changed = cached_signature_ != signature;
    if (!weapon_changed && !signature_changed && applied_)
        return;

    if (weapon_changed || signature_changed || cached_materials_.empty())
    {
        cached_weapon_key_ = weapon_key;
        rebuild_material_cache(unity_image, main_player);
    }

    cached_signature_ = signature;
    applied_ = apply_cached_materials(unity_image, config);
}

void WeaponChamsService::reset()
{
    restore_all();
    cached_weapon_key_ = nullptr;
    cached_signature_ = Signature{};
}

void WeaponChamsService::discard_session() noexcept
{
    cached_materials_.clear();
    cached_weapon_key_ = nullptr;
    cached_signature_ = Signature{};
    applied_ = false;
}
}
