#include "adapters/eft/items/actions/ItemPlacementService.h"
#include "adapters/eft/items/interop/ItemObjectAccess.h"

#include <cstddef>

#include "adapters/il2cpp/Il2CppResolver.h"
#include "adapters/il2cpp/Il2CppObjectAccess.h"
#include "adapters/il2cpp/Il2CppString.h"
#include "foundation/diagnostics/BuildOptions.h"
#include "foundation/diagnostics/Logger.h"

namespace
{
    constexpr int kPreferredContainerSlots[] = {4, 6, 5, 8};

    using GetObjectFn = Il2CppObject*(*)(Il2CppObject*);
    using GetSlotFn = Il2CppObject*(*)(Il2CppObject*, int);
    using FindFreeSpaceFn = Il2CppObject*(*)(Il2CppObject*, Il2CppObject*);
    using PlaceItemFn = void(*)(Il2CppObject*, Il2CppObject*, Il2CppObject*);

    void info(const char* message)
    {
        if (ta::foundation::diagnostics::build_options::verbose) ta::foundation::logger::info(message);
    }

    bool place_in_grid(Il2CppObject* grid, Il2CppObject* item, const char* success_log = nullptr)
    {
        if (!grid || !item) return false;

        ta::adapters::il2cpp::Il2CppObjectInstance instance(grid);
        const auto find_free_space = instance.get_method<FindFreeSpaceFn>("FindFreeSpace", 1);
        const auto place_item = instance.get_method<PlaceItemFn>("PlaceItem", 2);
        if (!find_free_space || !place_item) return false;

        Il2CppObject* space = find_free_space(grid, item);
        if (!space) return false;

        place_item(grid, item, space);
        if (success_log) info(success_log);
        return true;
    }

    bool place_in_named_grid(Il2CppObject* owner,
                             Il2CppObject* item,
                             const char* grid_getter,
                             const char* success_log)
    {
        if (!owner || !item) return false;
        ta::adapters::il2cpp::Il2CppObjectInstance instance(owner);
        const auto get_grid = instance.get_method<GetObjectFn>(grid_getter, 0);
        return get_grid && place_in_grid(get_grid(owner), item, success_log);
    }

    Il2CppObject* get_slot(Il2CppObject* equipment, const int slot_index)
    {
        if (!equipment) return nullptr;
        ta::adapters::il2cpp::Il2CppObjectInstance instance(equipment);
        const auto getter = instance.get_method<GetSlotFn>("GetSlot", 1);
        return getter ? getter(equipment, slot_index) : nullptr;
    }


    Il2CppObject* equipment(ta::adapters::il2cpp::Il2CppObjectInstance main_player)
    {
        auto inventory = ta::adapters::eft::items::ItemPlacementService::get_inventory(main_player);
        Il2CppObject* equipment_object = nullptr;
        return inventory.try_get_field("Equipment", equipment_object) ? equipment_object : nullptr;
    }
}

namespace ta::adapters::eft::items
{
    bool ItemPlacementService::place_item_in_grids(Il2CppObject* slot_item, Il2CppObject* item)
    {
        if (!slot_item || !item) return false;

        ta::adapters::il2cpp::Il2CppObjectInstance instance(slot_item);
        Il2CppArray* grids = nullptr;
        if (!instance.try_get_field("Grids", grids) || !grids)
            return false;

        for (std::size_t i = 0; i < grids->max_length; ++i)
        {
            Il2CppObject* grid = reinterpret_cast<Il2CppObject**>(&grids->data)[i];
            if (place_in_grid(grid, item)) return true;
        }
        return false;
    }

    ta::adapters::il2cpp::Il2CppObjectInstance ItemPlacementService::get_inventory(ta::adapters::il2cpp::Il2CppObjectInstance main_player)
    {
        Il2CppObject* controller_object = nullptr;
        if (!main_player.try_get_field("_inventoryController", controller_object) || !controller_object)
            return ta::adapters::il2cpp::Il2CppObjectInstance(nullptr);

        ta::adapters::il2cpp::Il2CppObjectInstance controller(controller_object);
        const auto getter = controller.get_method<GetObjectFn>("get_Inventory", 0);
        return ta::adapters::il2cpp::Il2CppObjectInstance(
            getter ? getter(controller.get_instance()) : nullptr);
    }

    bool ItemPlacementService::place_in_equipment(ta::adapters::il2cpp::Il2CppObjectInstance main_player,
                                                   Il2CppObject* item,
                                                   const int slot_index,
                                                   const bool overwrite_slot)
    {
        if (!item) return false;

        Il2CppObject* slot = get_slot(equipment(main_player), slot_index);
        if (!slot) return false;

        ta::adapters::il2cpp::Il2CppObjectInstance slot_instance(slot);
        if (overwrite_slot)
        {
            const auto add = slot_instance.get_method<Il2CppObject*(*)(Il2CppObject*, Il2CppObject*)>("Add", 1);
            if (!add) return false;
            add(slot, item);
            info("Overwrote equipment slot");
            return true;
        }

        const bool placed = place_item_in_grids(ta::adapters::eft::items::object_access::contained_item(slot), item);
        if (placed) info("Placed Item");
        return placed;
    }

    bool ItemPlacementService::place_in_quest_inventory(ta::adapters::il2cpp::Il2CppObjectInstance main_player, Il2CppObject* item)
    {
        auto inventory = get_inventory(main_player);
        Il2CppObject* quest_items = nullptr;
        if (!inventory.try_get_field("QuestRaidItems", quest_items) || !quest_items)
            return false;
        return place_in_named_grid(quest_items, item, "get_Grid", "Placed Quest Item");
    }

    bool ItemPlacementService::place_in_container_slots(ta::adapters::il2cpp::Il2CppObjectInstance main_player, Il2CppObject* item)
    {
        Il2CppObject* player_equipment = equipment(main_player);
        for (const int slot_index : kPreferredContainerSlots)
        {
            if (place_item_in_grids(ta::adapters::eft::items::object_access::contained_item(get_slot(player_equipment, slot_index)), item))
                return true;
        }
        return false;
    }

    bool ItemPlacementService::place_into_transit_stash(ta::adapters::il2cpp::Il2CppObjectInstance game_world_instance,
                                                         ta::adapters::il2cpp::Il2CppObjectInstance main_player,
                                                         Il2CppObject* item)
    {
        if (!item) return false;

        Il2CppObject* transit_controller_object = nullptr;
        if (!game_world_instance.try_get_field(
                "<TransitController>k__BackingField", transit_controller_object) ||
            !transit_controller_object)
        {
            return false;
        }

        ta::adapters::il2cpp::Il2CppObjectInstance transit_controller(transit_controller_object);
        const auto get_transfer_controller = transit_controller.get_method<GetObjectFn>(
            "get_TransferItemsController", 0);
        if (!get_transfer_controller) return false;

        ta::adapters::il2cpp::Il2CppObjectInstance transfer_controller(get_transfer_controller(transit_controller.get_instance()));
        const auto get_profile_id = main_player.get_method<Il2CppString*(*)(Il2CppObject*)>("get_ProfileId", 0);
        const auto get_or_add_container = transfer_controller.get_method<Il2CppObject*(*)(Il2CppObject*, Il2CppString*)>(
            "GetOrAddTransferContainer", 1);
        const auto init_stash = transfer_controller.get_method<void(*)(Il2CppObject*, Il2CppObject*)>(
            "InitPlayerStash", 1);
        const auto move_items = transfer_controller.get_method<void(*)(Il2CppObject*, Il2CppObject*)>(
            "MoveItemsFromTempStashToTransferStash", 1);
        if (!get_profile_id || !get_or_add_container || !init_stash || !move_items)
            return false;

        Il2CppString* player_id = get_profile_id(main_player.get_instance());
        Il2CppObject* transit_stash = get_or_add_container(transfer_controller.get_instance(), player_id);
        if (!transit_stash) return false;

        init_stash(transfer_controller.get_instance(), main_player.get_instance());
        const bool placed = place_in_named_grid(transit_stash, item, "get_Grid", "Moved Transit Item");
        move_items(transfer_controller.get_instance(), main_player.get_instance());
        return placed;
    }
}
