/*
 * Copyright (C) 2016+ AzerothCore <www.azerothcore.org>, released under GNU AGPL v3 license, you may redistribute it
 * and/or modify it under version 3 of the License, or (at your option), any later version.
 */

#include "RandomItemMgr.h"

#include "ItemTemplate.h"
#include "LootValues.h"
#include "Playerbots.h"

char* strstri(char const* str1, char const* str2);
std::set<uint32> RandomItemMgr::itemCache;

uint64 BotEquipKey::GetKey() { return level + 100 * clazz + 10000 * slot + 1000000 * quality; }

class RandomItemGuildTaskPredicate : public RandomItemPredicate
{
public:
    bool Apply(ItemTemplate const* proto) override
    {
        if (proto->Bonding == BIND_WHEN_PICKED_UP || proto->Bonding == BIND_QUEST_ITEM ||
            proto->Bonding == BIND_WHEN_USE)
            return false;

        if (proto->Quality < ITEM_QUALITY_NORMAL)
            return false;

        if ((proto->Class == ITEM_CLASS_ARMOR || proto->Class == ITEM_CLASS_WEAPON) &&
            proto->Quality >= ITEM_QUALITY_RARE)
            return true;

        if (proto->Class == ITEM_CLASS_TRADE_GOODS || proto->Class == ITEM_CLASS_CONSUMABLE)
            return true;

        return false;
    }
};

class RandomItemGuildTaskRewardPredicate : public RandomItemPredicate
{
public:
    RandomItemGuildTaskRewardPredicate(bool equip, bool rare) : equip(equip), rare(rare) {}

    bool Apply(ItemTemplate const* proto) override
    {
        if (proto->Bonding == BIND_WHEN_PICKED_UP || proto->Bonding == BIND_QUEST_ITEM ||
            proto->Bonding == BIND_WHEN_USE)
            return false;

        if (proto->Class == ITEM_CLASS_QUEST)
            return false;

        if (equip)
        {
            uint32 desiredQuality = rare ? ITEM_QUALITY_RARE : ITEM_QUALITY_UNCOMMON;
            if (proto->Quality < desiredQuality || proto->Quality >= ITEM_QUALITY_EPIC)
                return false;

            if (proto->Class == ITEM_CLASS_ARMOR || proto->Class == ITEM_CLASS_WEAPON)
                return true;
        }
        else
        {
            uint32 desiredQuality = rare ? ITEM_QUALITY_UNCOMMON : ITEM_QUALITY_NORMAL;
            if (proto->Quality < desiredQuality || proto->Quality >= ITEM_QUALITY_RARE)
                return false;

            if (proto->Class == ITEM_CLASS_TRADE_GOODS || proto->Class == ITEM_CLASS_CONSUMABLE)
                return true;
        }

        return false;
    }

private:
    bool equip;
    bool rare;
};

RandomItemMgr::RandomItemMgr()
{
    predicates[RANDOM_ITEM_GUILD_TASK] = new RandomItemGuildTaskPredicate();
    predicates[RANDOM_ITEM_GUILD_TASK_REWARD_EQUIP_GREEN] = new RandomItemGuildTaskRewardPredicate(true, false);
    predicates[RANDOM_ITEM_GUILD_TASK_REWARD_EQUIP_BLUE] = new RandomItemGuildTaskRewardPredicate(true, true);
    predicates[RANDOM_ITEM_GUILD_TASK_REWARD_TRADE] = new RandomItemGuildTaskRewardPredicate(false, false);
    predicates[RANDOM_ITEM_GUILD_TASK_REWARD_TRADE_RARE] = new RandomItemGuildTaskRewardPredicate(false, true);

    viableSlots[EQUIPMENT_SLOT_HEAD].insert(INVTYPE_HEAD);
    viableSlots[EQUIPMENT_SLOT_NECK].insert(INVTYPE_NECK);
    viableSlots[EQUIPMENT_SLOT_SHOULDERS].insert(INVTYPE_SHOULDERS);
    viableSlots[EQUIPMENT_SLOT_BODY].insert(INVTYPE_BODY);
    viableSlots[EQUIPMENT_SLOT_CHEST].insert(INVTYPE_CHEST);
    viableSlots[EQUIPMENT_SLOT_CHEST].insert(INVTYPE_ROBE);
    viableSlots[EQUIPMENT_SLOT_WAIST].insert(INVTYPE_WAIST);
    viableSlots[EQUIPMENT_SLOT_LEGS].insert(INVTYPE_LEGS);
    viableSlots[EQUIPMENT_SLOT_FEET].insert(INVTYPE_FEET);
    viableSlots[EQUIPMENT_SLOT_WRISTS].insert(INVTYPE_WRISTS);
    viableSlots[EQUIPMENT_SLOT_HANDS].insert(INVTYPE_HANDS);
    viableSlots[EQUIPMENT_SLOT_FINGER1].insert(INVTYPE_FINGER);
    viableSlots[EQUIPMENT_SLOT_FINGER2].insert(INVTYPE_FINGER);
    viableSlots[EQUIPMENT_SLOT_TRINKET1].insert(INVTYPE_TRINKET);
    viableSlots[EQUIPMENT_SLOT_TRINKET2].insert(INVTYPE_TRINKET);
    viableSlots[EQUIPMENT_SLOT_MAINHAND].insert(INVTYPE_WEAPON);
    viableSlots[EQUIPMENT_SLOT_MAINHAND].insert(INVTYPE_2HWEAPON);
    viableSlots[EQUIPMENT_SLOT_MAINHAND].insert(INVTYPE_WEAPONMAINHAND);
    viableSlots[EQUIPMENT_SLOT_OFFHAND].insert(INVTYPE_WEAPON);
    viableSlots[EQUIPMENT_SLOT_OFFHAND].insert(INVTYPE_2HWEAPON);
    viableSlots[EQUIPMENT_SLOT_OFFHAND].insert(INVTYPE_SHIELD);
    viableSlots[EQUIPMENT_SLOT_OFFHAND].insert(INVTYPE_WEAPONMAINHAND);
    viableSlots[EQUIPMENT_SLOT_OFFHAND].insert(INVTYPE_HOLDABLE);
    viableSlots[EQUIPMENT_SLOT_RANGED].insert(INVTYPE_RANGED);
    viableSlots[EQUIPMENT_SLOT_RANGED].insert(INVTYPE_THROWN);
    viableSlots[EQUIPMENT_SLOT_RANGED].insert(INVTYPE_RANGEDRIGHT);
    viableSlots[EQUIPMENT_SLOT_RANGED].insert(INVTYPE_RELIC);
    viableSlots[EQUIPMENT_SLOT_TABARD].insert(INVTYPE_TABARD);
    viableSlots[EQUIPMENT_SLOT_BACK].insert(INVTYPE_CLOAK);

    weightStatLink["sta"] = ITEM_MOD_STAMINA;
    weightStatLink["str"] = ITEM_MOD_STRENGTH;
    weightStatLink["agi"] = ITEM_MOD_AGILITY;
    weightStatLink["int"] = ITEM_MOD_INTELLECT;
    weightStatLink["spi"] = ITEM_MOD_SPIRIT;

    weightStatLink["splpwr"] = ITEM_MOD_SPELL_POWER;
    weightStatLink["atkpwr"] = ITEM_MOD_ATTACK_POWER;

    weightStatLink["exprtng"] = ITEM_MOD_EXPERTISE_RATING;
    weightStatLink["critstrkrtng"] = ITEM_MOD_CRIT_RATING;
    weightStatLink["hitrtng"] = ITEM_MOD_HIT_RATING;
    weightStatLink["hastertng"] = ITEM_MOD_HASTE_RATING;
    weightStatLink["armorpenrtng"] = ITEM_MOD_ARMOR_PENETRATION_RATING;

    weightStatLink["defrtng"] = ITEM_MOD_DEFENSE_SKILL_RATING;
    weightStatLink["dodgertng"] = ITEM_MOD_DODGE_RATING;
    weightStatLink["block"] = ITEM_MOD_BLOCK_VALUE;
    weightStatLink["blockrtng"] = ITEM_MOD_BLOCK_RATING;
    weightStatLink["parryrtng"] = ITEM_MOD_PARRY_RATING;

    weightStatLink["manargn"] = ITEM_MOD_MANA_REGENERATION;

    weightRatingLink["exprtng"] = CR_EXPERTISE;
    weightRatingLink["critstrkrtng"] = CR_CRIT_MELEE;
    weightRatingLink["hitrtng"] = CR_HIT_MELEE;
    weightRatingLink["hastertng"] = CR_HASTE_MELEE;
    weightRatingLink["armorpenrtng"] = CR_ARMOR_PENETRATION;

    weightRatingLink["defrtng"] = CR_DEFENSE_SKILL;
    weightRatingLink["dodgertng"] = CR_DODGE;
    weightRatingLink["blockrtng"] = CR_BLOCK;
    weightRatingLink["parryrtng"] = CR_PARRY;
}

void RandomItemMgr::Init()
{
    BuildItemInfoCache();
    // BuildEquipCache();
    BuildEquipCacheNew();
    BuildAmmoCache();
    BuildPotionCache();
    BuildFoodCache();
    BuildTradeCache();
    LoadEnchantmentPool();
}

void RandomItemMgr::InitAfterAhBot()
{
    BuildRandomItemCache();
    // BuildRarityCache();
}

RandomItemMgr::~RandomItemMgr()
{
    for (std::map<RandomItemType, RandomItemPredicate*>::iterator i = predicates.begin(); i != predicates.end(); ++i)
        delete i->second;

    predicates.clear();
}

bool RandomItemMgr::HandleConsoleCommand(ChatHandler* /*handler*/, char const* args)
{
    if (!args || !*args)
    {
        LOG_ERROR("playerbots", "Usage: rnditem");
        return false;
    }

    return false;
}

RandomItemList RandomItemMgr::Query(uint32 level, RandomItemType type, RandomItemPredicate* predicate)
{
    RandomItemList& list = randomItemCache[(level - 1) / 10][type];

    RandomItemList result;
    for (RandomItemList::iterator i = list.begin(); i != list.end(); ++i)
    {
        uint32 itemId = *i;
        ItemTemplate const* proto = sObjectMgr->GetItemTemplate(itemId);
        if (!proto)
            continue;

        if (predicate && !predicate->Apply(proto))
            continue;

        result.push_back(itemId);
    }

    return result;
}

void RandomItemMgr::BuildRandomItemCache()
{
    if (PreparedQueryResult result =
            PlayerbotsDatabase.Query(PlayerbotsDatabase.GetPreparedStatement(PLAYERBOTS_SEL_RNDITEM_CACHE)))
    {
        LOG_INFO("server.loading", "Loading random item cache");
        uint32 count = 0;
        do
        {
            Field* fields = result->Fetch();
            uint32 level = fields[0].Get<uint32>();
            uint32 type = fields[1].Get<uint32>();
            uint32 itemId = fields[2].Get<uint32>();

            RandomItemType rit = (RandomItemType)type;
            randomItemCache[level][rit].push_back(itemId);
            ++count;

        } while (result->NextRow());

        LOG_INFO("server.loading", "Equipment cache loaded from {} records", count);
    }
    else
    {
        ItemTemplateContainer const* itemTemplates = sObjectMgr->GetItemTemplateStore();
        LOG_INFO("server.loading", "Building random item cache from {} items", itemTemplates->size());

        for (auto const& itr : *itemTemplates)
        {
            ItemTemplate const* proto = &itr.second;
            if (!proto)
                continue;

            if (proto->Duration & 0x80000000)
                continue;

            if (strstri(proto->Name1.c_str(), "qa") || strstri(proto->Name1.c_str(), "test") ||
                strstri(proto->Name1.c_str(), "deprecated"))
                continue;

            if (!proto->ItemLevel)
                continue;

            if (!proto->SellPrice)
                continue;

            uint32 level = proto->ItemLevel;
            for (uint32 type = RANDOM_ITEM_GUILD_TASK; type <= RANDOM_ITEM_GUILD_TASK_REWARD_TRADE_RARE; type++)
            {
                RandomItemType rit = (RandomItemType)type;
                if (predicates[rit] && !predicates[rit]->Apply(proto))
                    continue;

                randomItemCache[level / 10][rit].push_back(itr.first);

                PlayerbotsDatabasePreparedStatement* stmt =
                    PlayerbotsDatabase.GetPreparedStatement(PLAYERBOTS_INS_RNDITEM_CACHE);
                stmt->SetData(0, level / 10);
                stmt->SetData(1, type);
                stmt->SetData(2, itr.first);
                PlayerbotsDatabase.Execute(stmt);
            }
        }

        uint32 maxLevel = sPlayerbotAIConfig.randomBotMaxLevel;
        if (maxLevel > sWorld->getIntConfig(CONFIG_MAX_PLAYER_LEVEL))
            maxLevel = sWorld->getIntConfig(CONFIG_MAX_PLAYER_LEVEL);

        for (uint32 level = 0; level < maxLevel / 10; level++)
        {
            for (uint32 type = RANDOM_ITEM_GUILD_TASK; type <= RANDOM_ITEM_GUILD_TASK_REWARD_TRADE_RARE; type++)
            {
                RandomItemList list = randomItemCache[level][(RandomItemType)type];
                LOG_INFO("playerbots", "    Level {}..{} Type {} - {} random items cached", level * 10, level * 10 + 9,
                         type, list.size());

                for (RandomItemList::iterator i = list.begin(); i != list.end(); ++i)
                {
                    uint32 itemId = *i;
                    ItemTemplate const* proto = sObjectMgr->GetItemTemplate(itemId);
                    if (!proto)
                        continue;

                    LOG_DEBUG("playerbots", "        [{}] {}", itemId, proto->Name1.c_str());
                }
            }
        }
    }
}

uint32 RandomItemMgr::GetRandomItem(uint32 level, RandomItemType type, RandomItemPredicate* predicate)
{
    RandomItemList const& list = Query(level, type, predicate);
    if (list.empty())
        return 0;

    uint32 index = urand(0, list.size() - 1);
    uint32 itemId = list[index];

    return itemId;
}

bool RandomItemMgr::CanEquipItem(BotEquipKey key, ItemTemplate const* proto)
{
    if (proto->Duration & 0x80000000)
        return false;

    if (proto->Quality != key.quality)
        return false;

    if (proto->Bonding == BIND_QUEST_ITEM || proto->Bonding == BIND_WHEN_USE)
        return false;

    if (proto->Class == ITEM_CLASS_CONTAINER)
        return true;

    std::set<InventoryType> slots = viableSlots[(EquipmentSlots)key.slot];
    if (slots.find((InventoryType)proto->InventoryType) == slots.end())
        return false;

    uint32 requiredLevel = proto->RequiredLevel;
    if (!requiredLevel)
    {
        requiredLevel = GetMinLevelFromCache(proto->ItemId);
    }

    if (!requiredLevel)
        requiredLevel = key.level;

    uint32 level = key.level;

    uint32 delta = 2;
    if (level < 15)
        delta = 15;
    else if (level < 40)
        delta = 10;  // urand(5, 10);
    else if (level < 60)
        delta = 6;  // urand(3, 7);
    else if (level < 70)
        delta = 9;  // urand(2, 5);
    else if (level < 80)
        delta = 9;  // urand(2, 4);
    else if (level == 80)
        delta = 9;  // urand(2, 4);

    if (key.quality > ITEM_QUALITY_NORMAL && (requiredLevel > level || requiredLevel < level - delta))
        return false;

    // for (uint32 gap = 60; gap <= 80; gap += 10)
    // {
    //     if (level > gap && requiredLevel <= gap)
    //         return false;
    // }

    return true;
}

bool RandomItemMgr::CanEquipItemNew(ItemTemplate const* proto)
{
    if (proto->Duration & 0x80000000)
        return false;

    if (proto->Bonding == BIND_QUEST_ITEM || proto->Bonding == BIND_WHEN_USE)
        return false;

    if (proto->Class == ITEM_CLASS_CONTAINER)
        return false;

    bool properSlot = false;
    for (std::map<EquipmentSlots, std::set<InventoryType> >::iterator i = viableSlots.begin(); i != viableSlots.end();
         ++i)
    {
        std::set<InventoryType> const& slots = viableSlots[(EquipmentSlots)i->first];
        if (slots.find((InventoryType)proto->InventoryType) != slots.end())
            properSlot = true;
    }

    return properSlot;
}

void RandomItemMgr::AddItemStats(uint32 mod, uint8& sp, uint8& ap, uint8& tank)
{
    switch (mod)
    {
        case ITEM_MOD_HEALTH:
        case ITEM_MOD_STAMINA:
        case ITEM_MOD_MANA:
        case ITEM_MOD_INTELLECT:
        case ITEM_MOD_SPIRIT:
            ++sp;
            break;
    }

    switch (mod)
    {
        case ITEM_MOD_AGILITY:
        case ITEM_MOD_STRENGTH:
        case ITEM_MOD_HEALTH:
        case ITEM_MOD_STAMINA:
            ++tank;
            break;
    }

    switch (mod)
    {
        case ITEM_MOD_HEALTH:
        case ITEM_MOD_STAMINA:
        case ITEM_MOD_AGILITY:
        case ITEM_MOD_STRENGTH:
            ++ap;
            break;
    }
}

bool RandomItemMgr::CheckItemStats(uint8 clazz, uint8 sp, uint8 ap, uint8 tank)
{
    switch (clazz)
    {
        case CLASS_PRIEST:
        case CLASS_MAGE:
        case CLASS_WARLOCK:
            if (!sp || ap > sp || tank > sp)
                return false;
            break;
        case CLASS_PALADIN:
        case CLASS_WARRIOR:
            if ((!ap && !tank) || sp > ap || sp > tank)
                return false;
            break;
        case CLASS_HUNTER:
        case CLASS_ROGUE:
            if (!ap || sp > ap || sp > tank)
                return false;
            break;
    }

    return sp || ap || tank;
}

std::vector<uint32> RandomItemMgr::GetCachedEquipments(uint32 requiredLevel, uint32 inventoryType)
{
    return equipCacheNew[requiredLevel][inventoryType];
}

void RandomItemMgr::LoadEnchantmentPool()
{
    enchPoolCache.clear();

    QueryResult result = WorldDatabase.Query("SELECT entry, ench FROM item_enchantment_template");
    if (!result)
    {
        LOG_WARN("playerbots", "item_enchantment_template empty; bot autogear cannot evaluate random suffixes");
        return;
    }

    uint32 count = 0;
    do
    {
        Field* fields = result->Fetch();
        uint32 entry = fields[0].Get<uint32>();
        uint32 ench = fields[1].Get<uint32>();
        enchPoolCache[entry].push_back(ench);
        ++count;
    } while (result->NextRow());

    LOG_INFO("playerbots", "Loaded {} item enchantment pool rows for bot autogear", count);
}

std::vector<uint32> const& RandomItemMgr::GetEnchantmentPool(uint32 entry) const
{
    static std::vector<uint32> const empty;
    auto it = enchPoolCache.find(entry);
    if (it == enchPoolCache.end())
        return empty;
    return it->second;
}

bool RandomItemMgr::ShouldEquipArmorForSpec(uint8 playerclass, uint8 spec, ItemTemplate const* proto)
{
    if (proto->InventoryType == INVTYPE_TABARD)
        return true;

    if (!m_weightScales[playerclass][spec].info.id)
        return false;

    std::unordered_set<uint32> resultArmorSubClass = {ITEM_SUBCLASS_ARMOR_CLOTH};

    switch (playerclass)
    {
        case CLASS_WARRIOR:
        {
            if (proto->InventoryType == INVTYPE_HOLDABLE)
                return false;

            if (m_weightScales[playerclass][spec].info.name == "arms" ||
                m_weightScales[playerclass][spec].info.name == "fury")
            {
                resultArmorSubClass = {ITEM_SUBCLASS_ARMOR_LEATHER, ITEM_SUBCLASS_ARMOR_MAIL,
                                       ITEM_SUBCLASS_ARMOR_PLATE};
            }
            else
                resultArmorSubClass = {ITEM_SUBCLASS_ARMOR_MAIL, ITEM_SUBCLASS_ARMOR_PLATE};
            break;
        }
        case CLASS_DEATH_KNIGHT:
        {
            if (proto->InventoryType == INVTYPE_HOLDABLE)
                return false;

            resultArmorSubClass = {ITEM_SUBCLASS_ARMOR_SIGIL, ITEM_SUBCLASS_ARMOR_PLATE};
        }
        case CLASS_PALADIN:
        {
            if (m_weightScales[playerclass][spec].info.name != "holy" && proto->InventoryType == INVTYPE_HOLDABLE)
                return false;

            if (m_weightScales[playerclass][spec].info.name != "holy")
                resultArmorSubClass = {ITEM_SUBCLASS_ARMOR_MAIL, ITEM_SUBCLASS_ARMOR_PLATE, ITEM_SUBCLASS_ARMOR_LIBRAM};
            else
                resultArmorSubClass = {ITEM_SUBCLASS_ARMOR_CLOTH, ITEM_SUBCLASS_ARMOR_LEATHER, ITEM_SUBCLASS_ARMOR_MAIL,
                                       ITEM_SUBCLASS_ARMOR_PLATE, ITEM_SUBCLASS_ARMOR_LIBRAM};
            break;
        }
        case CLASS_HUNTER:
        {
            if (proto->InventoryType == INVTYPE_HOLDABLE)
                return false;

            resultArmorSubClass = {ITEM_SUBCLASS_ARMOR_CLOTH, ITEM_SUBCLASS_ARMOR_LEATHER, ITEM_SUBCLASS_ARMOR_MAIL};
            break;
        }
        case CLASS_ROGUE:
        {
            if (proto->InventoryType == INVTYPE_HOLDABLE)
                return false;

            resultArmorSubClass = {ITEM_SUBCLASS_ARMOR_CLOTH, ITEM_SUBCLASS_ARMOR_LEATHER};
            break;
        }
        case CLASS_PRIEST:
        {
            resultArmorSubClass = {ITEM_SUBCLASS_ARMOR_CLOTH};
            break;
        }
        case CLASS_SHAMAN:
        {
            if (m_weightScales[playerclass][spec].info.name == "enhance" && proto->InventoryType == INVTYPE_HOLDABLE)
                return false;

            resultArmorSubClass = {ITEM_SUBCLASS_ARMOR_TOTEM, ITEM_SUBCLASS_ARMOR_CLOTH, ITEM_SUBCLASS_ARMOR_LEATHER,
                                   ITEM_SUBCLASS_ARMOR_MAIL};
            break;
        }
        case CLASS_MAGE:
        case CLASS_WARLOCK:
        {
            resultArmorSubClass = {ITEM_SUBCLASS_ARMOR_CLOTH};
            break;
        }
        case CLASS_DRUID:
        {
            if ((m_weightScales[playerclass][spec].info.name == "feraltank" ||
                 m_weightScales[playerclass][spec].info.name == "feraldps") &&
                proto->InventoryType == INVTYPE_HOLDABLE)
                return false;

            resultArmorSubClass = {ITEM_SUBCLASS_ARMOR_IDOL, ITEM_SUBCLASS_ARMOR_CLOTH, ITEM_SUBCLASS_ARMOR_LEATHER};
            break;
        }
    }

    return resultArmorSubClass.find(proto->SubClass) != resultArmorSubClass.end();
}

bool RandomItemMgr::ShouldEquipWeaponForSpec(uint8 playerclass, uint8 spec, ItemTemplate const* proto)
{
    EquipmentSlots slot_mh = EQUIPMENT_SLOT_START;
    EquipmentSlots slot_oh = EQUIPMENT_SLOT_START;
    EquipmentSlots slot_rh = EQUIPMENT_SLOT_START;
    for (std::map<EquipmentSlots, std::set<InventoryType> >::iterator i = viableSlots.begin(); i != viableSlots.end();
         ++i)
    {
        std::set<InventoryType> slots = viableSlots[(EquipmentSlots)i->first];
        if (slots.find((InventoryType)proto->InventoryType) != slots.end())
        {
            if (i->first == EQUIPMENT_SLOT_MAINHAND)
                slot_mh = i->first;
            if (i->first == EQUIPMENT_SLOT_OFFHAND)
                slot_oh = i->first;
            if (i->first == EQUIPMENT_SLOT_RANGED)
                slot_rh = i->first;
        }
    }

    if (slot_mh == EQUIPMENT_SLOT_START && slot_oh == EQUIPMENT_SLOT_START && slot_rh == EQUIPMENT_SLOT_START)
        return false;

    if (!m_weightScales[playerclass][spec].info.id)
        return false;

    std::unordered_set<uint32> mh_weapons;
    std::unordered_set<uint32> oh_weapons;
    std::unordered_set<uint32> r_weapons;

    switch (playerclass)
    {
        case CLASS_WARRIOR:
        {
            if (m_weightScales[playerclass][spec].info.name == "prot")
            {
                mh_weapons = {ITEM_SUBCLASS_WEAPON_SWORD, ITEM_SUBCLASS_WEAPON_AXE, ITEM_SUBCLASS_WEAPON_MACE,
                              ITEM_SUBCLASS_WEAPON_DAGGER, ITEM_SUBCLASS_WEAPON_FIST};
                oh_weapons = {ITEM_SUBCLASS_ARMOR_SHIELD};
                r_weapons = {ITEM_SUBCLASS_WEAPON_BOW, ITEM_SUBCLASS_WEAPON_CROSSBOW, ITEM_SUBCLASS_WEAPON_GUN};
            }
            else
            {
                mh_weapons = {ITEM_SUBCLASS_WEAPON_SWORD2, ITEM_SUBCLASS_WEAPON_AXE2, ITEM_SUBCLASS_WEAPON_MACE2,
                              ITEM_SUBCLASS_WEAPON_STAFF, ITEM_SUBCLASS_WEAPON_POLEARM};
                r_weapons = {ITEM_SUBCLASS_WEAPON_BOW, ITEM_SUBCLASS_WEAPON_CROSSBOW, ITEM_SUBCLASS_WEAPON_GUN};
            }
            break;
        }
        case CLASS_PALADIN:
        {
            if (m_weightScales[playerclass][spec].info.name == "prot")
            {
                mh_weapons = {ITEM_SUBCLASS_WEAPON_SWORD, ITEM_SUBCLASS_WEAPON_AXE, ITEM_SUBCLASS_WEAPON_MACE};
                oh_weapons = {ITEM_SUBCLASS_ARMOR_SHIELD};
                r_weapons = {ITEM_SUBCLASS_ARMOR_LIBRAM};
            }
            else if (m_weightScales[playerclass][spec].info.name == "holy")
            {
                mh_weapons = {ITEM_SUBCLASS_WEAPON_SWORD, ITEM_SUBCLASS_WEAPON_AXE, ITEM_SUBCLASS_WEAPON_MACE};
                oh_weapons = {ITEM_SUBCLASS_ARMOR_SHIELD, ITEM_SUBCLASS_ARMOR_MISC};
                r_weapons = {ITEM_SUBCLASS_ARMOR_LIBRAM};
            }
            else
            {
                mh_weapons = {ITEM_SUBCLASS_WEAPON_SWORD2, ITEM_SUBCLASS_WEAPON_AXE2, ITEM_SUBCLASS_WEAPON_MACE2,
                              ITEM_SUBCLASS_WEAPON_POLEARM};
                r_weapons = {ITEM_SUBCLASS_ARMOR_LIBRAM};
            }
            break;
        }
        case CLASS_HUNTER:
        {
            mh_weapons = {ITEM_SUBCLASS_WEAPON_SWORD2, ITEM_SUBCLASS_WEAPON_AXE2, ITEM_SUBCLASS_WEAPON_STAFF,
                          ITEM_SUBCLASS_WEAPON_POLEARM};
            r_weapons = {ITEM_SUBCLASS_WEAPON_BOW, ITEM_SUBCLASS_WEAPON_CROSSBOW, ITEM_SUBCLASS_WEAPON_GUN};
            break;
        }
        case CLASS_ROGUE:
        {
            mh_weapons = {ITEM_SUBCLASS_WEAPON_DAGGER};
            oh_weapons = {ITEM_SUBCLASS_WEAPON_DAGGER};
            r_weapons = {ITEM_SUBCLASS_WEAPON_BOW, ITEM_SUBCLASS_WEAPON_CROSSBOW, ITEM_SUBCLASS_WEAPON_GUN};
            break;
        }
        case CLASS_PRIEST:
        {
            mh_weapons = {ITEM_SUBCLASS_WEAPON_STAFF, ITEM_SUBCLASS_WEAPON_DAGGER, ITEM_SUBCLASS_WEAPON_MACE};
            oh_weapons = {ITEM_SUBCLASS_ARMOR_MISC};
            r_weapons = {ITEM_SUBCLASS_WEAPON_WAND};
            break;
        }
        case CLASS_SHAMAN:
        {
            if (m_weightScales[playerclass][spec].info.name == "resto")
            {
                mh_weapons = {ITEM_SUBCLASS_WEAPON_DAGGER, ITEM_SUBCLASS_WEAPON_AXE, ITEM_SUBCLASS_WEAPON_MACE,
                              ITEM_SUBCLASS_WEAPON_FIST};
                oh_weapons = {ITEM_SUBCLASS_ARMOR_MISC, ITEM_SUBCLASS_ARMOR_SHIELD};
                r_weapons = {ITEM_SUBCLASS_ARMOR_TOTEM};
            }
            else if (m_weightScales[playerclass][spec].info.name == "enhance")
            {
                mh_weapons = {ITEM_SUBCLASS_WEAPON_MACE2, ITEM_SUBCLASS_WEAPON_AXE2, ITEM_SUBCLASS_WEAPON_DAGGER,
                              ITEM_SUBCLASS_WEAPON_AXE,   ITEM_SUBCLASS_WEAPON_MACE, ITEM_SUBCLASS_WEAPON_FIST};
                oh_weapons = {ITEM_SUBCLASS_ARMOR_SHIELD};
                r_weapons = {ITEM_SUBCLASS_ARMOR_TOTEM};
            }
            else
            {
                mh_weapons = {ITEM_SUBCLASS_WEAPON_STAFF};
                r_weapons = {ITEM_SUBCLASS_ARMOR_TOTEM};
            }
            break;
        }
        case CLASS_MAGE:
        case CLASS_WARLOCK:
        {
            mh_weapons = {ITEM_SUBCLASS_WEAPON_STAFF, ITEM_SUBCLASS_WEAPON_DAGGER, ITEM_SUBCLASS_WEAPON_SWORD};
            oh_weapons = {ITEM_SUBCLASS_ARMOR_MISC};
            r_weapons = {ITEM_SUBCLASS_WEAPON_WAND};
            break;
        }
        case CLASS_DRUID:
        {
            if (m_weightScales[playerclass][spec].info.name == "feraltank")
            {
                mh_weapons = {ITEM_SUBCLASS_WEAPON_STAFF, ITEM_SUBCLASS_WEAPON_MACE2};
                r_weapons = {ITEM_SUBCLASS_ARMOR_IDOL};
            }
            else if (m_weightScales[playerclass][spec].info.name == "resto")
            {
                mh_weapons = {ITEM_SUBCLASS_WEAPON_STAFF, ITEM_SUBCLASS_WEAPON_DAGGER, ITEM_SUBCLASS_WEAPON_FIST,
                              ITEM_SUBCLASS_WEAPON_MACE};
                oh_weapons = {ITEM_SUBCLASS_ARMOR_MISC};
                r_weapons = {ITEM_SUBCLASS_ARMOR_IDOL};
            }
            else if (m_weightScales[playerclass][spec].info.name == "feraldps")
            {
                mh_weapons = {ITEM_SUBCLASS_WEAPON_STAFF, ITEM_SUBCLASS_WEAPON_MACE2};
                r_weapons = {ITEM_SUBCLASS_ARMOR_IDOL};
            }
            else
            {
                mh_weapons = {ITEM_SUBCLASS_WEAPON_STAFF};
                r_weapons = {ITEM_SUBCLASS_ARMOR_IDOL};
            }
            break;
        }
    }

    if (slot_mh == EQUIPMENT_SLOT_MAINHAND)
    {
        return mh_weapons.find(proto->SubClass) != mh_weapons.end();
    }

    if (slot_oh == EQUIPMENT_SLOT_OFFHAND)
    {
        return oh_weapons.find(proto->SubClass) != oh_weapons.end();
    }

    if (slot_rh == EQUIPMENT_SLOT_RANGED)
    {
        return r_weapons.find(proto->SubClass) != r_weapons.end();
    }

    return false;
}

bool RandomItemMgr::CanEquipArmor(uint8 clazz, uint32 level, ItemTemplate const* proto)
{
    if (proto->InventoryType == INVTYPE_TABARD)
        return true;

    if ((clazz == CLASS_WARRIOR || clazz == CLASS_PALADIN || clazz == CLASS_SHAMAN) &&
        proto->SubClass == ITEM_SUBCLASS_ARMOR_SHIELD)
        return true;

    if ((clazz == CLASS_WARRIOR || clazz == CLASS_PALADIN) && level >= 40)
    {
        if (proto->SubClass != ITEM_SUBCLASS_ARMOR_PLATE && proto->InventoryType != INVTYPE_CLOAK)
            return false;
    }

    if (((clazz == CLASS_WARRIOR || clazz == CLASS_PALADIN) && level < 40) ||
        ((clazz == CLASS_HUNTER || clazz == CLASS_SHAMAN) && level >= 40))
    {
        if (proto->SubClass != ITEM_SUBCLASS_ARMOR_MAIL && proto->InventoryType != INVTYPE_CLOAK)
            return false;
    }

    if (((clazz == CLASS_HUNTER || clazz == CLASS_SHAMAN) && level < 40) ||
        (clazz == CLASS_DRUID || clazz == CLASS_ROGUE))
    {
        if (proto->SubClass != ITEM_SUBCLASS_ARMOR_LEATHER && proto->InventoryType != INVTYPE_CLOAK)
            return false;
    }

    if (proto->Quality <= ITEM_QUALITY_NORMAL)
        return true;

    uint8 sp = 0, ap = 0, tank = 0;
    for (uint8 j = 0; j < MAX_ITEM_PROTO_STATS; ++j)
    {
        // for ItemStatValue != 0
        if (!proto->ItemStat[j].ItemStatValue)
            continue;

        AddItemStats(proto->ItemStat[j].ItemStatType, sp, ap, tank);
    }

    return CheckItemStats(clazz, sp, ap, tank);
}

bool RandomItemMgr::CanEquipWeapon(uint8 clazz, ItemTemplate const* proto)
{
    switch (clazz)
    {
        case CLASS_PRIEST:
            if (proto->SubClass != ITEM_SUBCLASS_WEAPON_STAFF && proto->SubClass != ITEM_SUBCLASS_WEAPON_WAND &&
                proto->SubClass != ITEM_SUBCLASS_WEAPON_MACE && proto->SubClass != ITEM_SUBCLASS_WEAPON_DAGGER)
                return false;
            break;
        case CLASS_MAGE:
        case CLASS_WARLOCK:
            if (proto->SubClass != ITEM_SUBCLASS_WEAPON_STAFF && proto->SubClass != ITEM_SUBCLASS_WEAPON_WAND &&
                proto->SubClass != ITEM_SUBCLASS_WEAPON_DAGGER && proto->SubClass != ITEM_SUBCLASS_WEAPON_SWORD)
                return false;
            break;
        case CLASS_WARRIOR:
            if (proto->SubClass != ITEM_SUBCLASS_WEAPON_MACE2 && proto->SubClass != ITEM_SUBCLASS_WEAPON_AXE &&
                proto->SubClass != ITEM_SUBCLASS_WEAPON_POLEARM && proto->SubClass != ITEM_SUBCLASS_WEAPON_SWORD2 &&
                proto->SubClass != ITEM_SUBCLASS_WEAPON_MACE && proto->SubClass != ITEM_SUBCLASS_WEAPON_SWORD &&
                proto->SubClass != ITEM_SUBCLASS_WEAPON_GUN && proto->SubClass != ITEM_SUBCLASS_WEAPON_CROSSBOW &&
                proto->SubClass != ITEM_SUBCLASS_WEAPON_BOW && proto->SubClass != ITEM_SUBCLASS_WEAPON_THROWN &&
                proto->SubClass != ITEM_SUBCLASS_WEAPON_AXE2 && proto->SubClass != ITEM_SUBCLASS_WEAPON_FIST &&
                proto->SubClass != ITEM_SUBCLASS_WEAPON_DAGGER && proto->SubClass != ITEM_SUBCLASS_WEAPON_STAFF)
                return false;
            break;
        case CLASS_PALADIN:
        case CLASS_DEATH_KNIGHT:
            if (proto->SubClass != ITEM_SUBCLASS_WEAPON_MACE2 && proto->SubClass != ITEM_SUBCLASS_WEAPON_POLEARM &&
                proto->SubClass != ITEM_SUBCLASS_WEAPON_SWORD2 && proto->SubClass != ITEM_SUBCLASS_WEAPON_AXE2 &&
                proto->SubClass != ITEM_SUBCLASS_WEAPON_AXE && proto->SubClass != ITEM_SUBCLASS_WEAPON_MACE &&
                proto->SubClass != ITEM_SUBCLASS_WEAPON_SWORD)
                return false;
            break;
        case CLASS_SHAMAN:
            if (proto->SubClass != ITEM_SUBCLASS_WEAPON_MACE && proto->SubClass != ITEM_SUBCLASS_WEAPON_AXE &&
                proto->SubClass != ITEM_SUBCLASS_WEAPON_FIST && proto->SubClass != ITEM_SUBCLASS_WEAPON_MACE2 &&
                proto->SubClass != ITEM_SUBCLASS_WEAPON_AXE2 && proto->SubClass != ITEM_SUBCLASS_WEAPON_DAGGER &&
                proto->SubClass != ITEM_SUBCLASS_WEAPON_STAFF)
                return false;
            break;
        case CLASS_DRUID:
            if (proto->SubClass != ITEM_SUBCLASS_WEAPON_MACE && proto->SubClass != ITEM_SUBCLASS_WEAPON_MACE2 &&
                proto->SubClass != ITEM_SUBCLASS_WEAPON_DAGGER && proto->SubClass != ITEM_SUBCLASS_WEAPON_STAFF &&
                proto->SubClass != ITEM_SUBCLASS_WEAPON_POLEARM)
                return false;
            break;
        case CLASS_HUNTER:
            if (proto->SubClass != ITEM_SUBCLASS_WEAPON_DAGGER && proto->SubClass != ITEM_SUBCLASS_WEAPON_BOW &&
                proto->SubClass != ITEM_SUBCLASS_WEAPON_AXE2 && proto->SubClass != ITEM_SUBCLASS_WEAPON_AXE &&
                proto->SubClass != ITEM_SUBCLASS_WEAPON_SWORD2 && proto->SubClass != ITEM_SUBCLASS_WEAPON_SWORD &&
                proto->SubClass != ITEM_SUBCLASS_WEAPON_FIST && proto->SubClass != ITEM_SUBCLASS_WEAPON_GUN &&
                proto->SubClass != ITEM_SUBCLASS_WEAPON_CROSSBOW && proto->SubClass != ITEM_SUBCLASS_WEAPON_STAFF &&
                proto->SubClass != ITEM_SUBCLASS_WEAPON_POLEARM)
                return false;
            break;
        case CLASS_ROGUE:
            if (proto->SubClass != ITEM_SUBCLASS_WEAPON_DAGGER && proto->SubClass != ITEM_SUBCLASS_WEAPON_SWORD &&
                proto->SubClass != ITEM_SUBCLASS_WEAPON_FIST && proto->SubClass != ITEM_SUBCLASS_WEAPON_MACE &&
                proto->SubClass != ITEM_SUBCLASS_WEAPON_GUN && proto->SubClass != ITEM_SUBCLASS_WEAPON_CROSSBOW &&
                proto->SubClass != ITEM_SUBCLASS_WEAPON_BOW && proto->SubClass != ITEM_SUBCLASS_WEAPON_THROWN &&
                proto->SubClass != ITEM_SUBCLASS_WEAPON_AXE)
                return false;
            break;
    }

    return true;
}

void RandomItemMgr::BuildItemInfoCache()
{
    //uint32 maxLevel = sWorld->getIntConfig(CONFIG_MAX_PLAYER_LEVEL); //not used, line marked for removal.

    // load weightscales
    LOG_INFO("playerbots", "Loading weightscales info");

    uint32 counter = 1;
    uint32 totalcount = 0;
    uint32 statcount = 0;
    uint32 curClass = CLASS_WARRIOR;

    PlayerbotsDatabasePreparedStatement* stmt = PlayerbotsDatabase.GetPreparedStatement(PLAYERBOTS_SEL_WEIGHTSCALES);
    if (PreparedQueryResult result = PlayerbotsDatabase.Query(stmt))
    {
        do
        {
            Field* fields = result->Fetch();
            uint32 id = fields[0].Get<uint32>();
            std::string name = fields[1].Get<std::string>();
            uint8 clazz = fields[2].Get<uint8>();

            if (clazz != curClass)
            {
                counter = 1;
                curClass = clazz;
            }

            WeightScale scale;
            scale.info.id = id;
            scale.info.name = name;
            m_weightScales[clazz][counter] = std::move(scale);
            counter++;
            totalcount++;

        } while (result->NextRow());

        LOG_INFO("playerbots", "Loaded {} weightscale class specs", totalcount);
    }

    stmt = PlayerbotsDatabase.GetPreparedStatement(PLAYERBOTS_SEL_WEIGHTSCALE_DATA);
    if (PreparedQueryResult result = PlayerbotsDatabase.Query(stmt))
    {
        do
        {
            Field* fields = result->Fetch();
            uint32 id = fields[0].Get<uint32>();
            std::string field = fields[1].Get<std::string>();
            uint32 weight = fields[2].Get<uint32>();

            WeightScaleStat stat;
            stat.stat = field;
            stat.weight = weight;

            for (uint8 cls = CLASS_WARRIOR; cls < MAX_CLASSES; ++cls)
            {
                for (uint32 spec = 1; spec < 5; ++spec)
                {
                    if (m_weightScales[cls][spec].info.id == id)
                        m_weightScales[cls][spec].stats.push_back(std::move(stat));
                }
            }
            statcount++;

        } while (result->NextRow());

        LOG_INFO("playerbots", "Loaded {} weightscale stat weights", statcount);
    }

    if (m_weightScales[1].empty())
    {
        LOG_ERROR("playerbots", "Error loading item weight scales");
        return;
    }

    // vendor items
    LOG_INFO("playerbots", "Loading vendor item list...");

    std::set<uint32> vendorItems;
    vendorItems.clear();
    if (QueryResult result = WorldDatabase.Query("SELECT item FROM npc_vendor"))
    {
        do
        {
            Field* fields = result->Fetch();
            int32 entry = fields[0].Get<int32>();
            if (entry <= 0)
                continue;

            vendorItems.insert(entry);
        } while (result->NextRow());
    }

    LOG_INFO("playerbots", "Loaded {} vendor items...", vendorItems.size());

    // calculate drop source
    LOG_INFO("playerbots", "Loading loot templates...");
    DropMap* dropMap = new DropMap;

    if (CreatureTemplateContainer const* creatures = sObjectMgr->GetCreatureTemplates())
    {
        for (CreatureTemplateContainer::const_iterator itr = creatures->begin(); itr != creatures->end(); ++itr)
        {
            uint32 sEntry = itr->first;
            if (LootTemplateAccess const* lTemplateA =
                    DropMapValue::GetLootTemplate(ObjectGuid::Create<HighGuid::Unit>(sEntry, uint32(1)), LOOT_CORPSE))
                for (auto const& lItem : lTemplateA->Entries)
                    dropMap->insert(std::make_pair(lItem->itemid, sEntry));
        }
    }

    if (GameObjectTemplateContainer const* gameobjects = sObjectMgr->GetGameObjectTemplates())
    {
        for (auto const& itr : *gameobjects)
        {
            uint32 sEntry = itr.first;
            if (LootTemplateAccess const* lTemplateA = DropMapValue::GetLootTemplate(
                    ObjectGuid::Create<HighGuid::GameObject>(sEntry, uint32(1)), LOOT_CORPSE))
                for (auto const& lItem : lTemplateA->Entries)
                    dropMap->insert(std::make_pair(lItem->itemid, sEntry));
        }
    }

    LOG_INFO("playerbots", "Loaded {} loot templates...", dropMap->size());

    ItemTemplateContainer const* itemTemplate = sObjectMgr->GetItemTemplateStore();
    LOG_INFO("playerbots", "Calculating stat weights for {} items...", itemTemplate->size());

    PlayerbotsDatabaseTransaction trans = PlayerbotsDatabase.BeginTransaction();

    for (auto const& itr : *itemTemplate)
    {
        ItemTemplate const* proto = &itr.second;
        if (!proto)
            continue;

        // skip test items
        if (strstr(proto->Name1.c_str(), "(Test)") || strstr(proto->Name1.c_str(), "(TEST)") ||
            strstr(proto->Name1.c_str(), "(test)") || strstr(proto->Name1.c_str(), "(JEFFTEST)") ||
            strstr(proto->Name1.c_str(), "Test ") || strstr(proto->Name1.c_str(), "Test") ||
            strstr(proto->Name1.c_str(), "TEST") || strstr(proto->Name1.c_str(), "TEST ") ||
            strstr(proto->Name1.c_str(), " TEST") || strstr(proto->Name1.c_str(), "2200 ") ||
            strstr(proto->Name1.c_str(), "Deprecated ") || strstr(proto->Name1.c_str(), "Unused ") ||
            strstr(proto->Name1.c_str(), "Monster ") || strstr(proto->Name1.c_str(), "[PH]") ||
            strstr(proto->Name1.c_str(), "(OLD)") || strstr(proto->Name1.c_str(), "QR") ||
            strstr(proto->Name1.c_str(), "zzOLD"))
        {
            itemForTest.insert(proto->ItemId);
            continue;
        }

        if (proto->HasFlag(ITEM_FLAG_DEPRECATED))
        {
            itemForTest.insert(proto->ItemId);
            continue;
        }
        // skip items with rank/rep requirements
        /*if (proto->RequiredHonorRank > 0 ||
            proto->RequiredSkillRank > 0 ||
            proto->RequiredCityRank > 0 ||
            proto->RequiredReputationRank > 0)
            continue;*/

        // if (proto->RequiredHonorRank > 0 || proto->RequiredSkillRank > 0 || proto->RequiredCityRank > 0)
        //     continue;

        // // skip random enchant items
        // if (proto->RandomProperty)
        //     continue;

        // // skip heirloom items
        // if (proto->Quality == ITEM_QUALITY_HEIRLOOM)
        //     continue;

        // // check possible equip slots
        // EquipmentSlots slot = EQUIPMENT_SLOT_START;
        // for (std::map<EquipmentSlots, std::set<InventoryType> >::iterator i = viableSlots.begin();
        //      i != viableSlots.end(); ++i)
        // {
        //     std::set<InventoryType> slots = viableSlots[(EquipmentSlots)i->first];
        //     if (slots.find((InventoryType)proto->InventoryType) != slots.end())
        //         slot = i->first;
        // }

        // if (slot == EQUIPMENT_SLOT_START)
        //     continue;

        // Init Item cache
        // ItemInfoEntry cacheInfo;

        // for (uint8 clazz = CLASS_WARRIOR; clazz < MAX_CLASSES; ++clazz)
        // {
        //     // skip nonexistent classes
        //     if (!((1 << (clazz - 1)) & CLASSMASK_ALL_PLAYABLE) || !sChrClassesStore.LookupEntry(clazz))
        //         continue;

        //     // skip wrong classes
        //     if ((proto->AllowableClass & (1 << (clazz - 1))) == 0)
        //         continue;

        //     for (uint32 spec = 1; spec < 5; ++spec)
        //     {
        //         if (!m_weightScales[clazz][spec].info.id)
        //             continue;

        //         // check possible armor for spec
        //         if (m_weightScales)
        //         if (proto->Class == ITEM_CLASS_ARMOR && (
        //             slot == EQUIPMENT_SLOT_HEAD ||
        //             slot == EQUIPMENT_SLOT_SHOULDERS ||
        //             slot == EQUIPMENT_SLOT_CHEST ||
        //             slot == EQUIPMENT_SLOT_WAIST ||
        //             slot == EQUIPMENT_SLOT_LEGS ||
        //             slot == EQUIPMENT_SLOT_FEET ||
        //             slot == EQUIPMENT_SLOT_WRISTS ||
        //             slot == EQUIPMENT_SLOT_HANDS) &&
        //             !ShouldEquipArmorForSpec(clazz, spec, proto))
        //             continue;

        //         // check possible weapon for spec
        //         if ((proto->Class == ITEM_CLASS_WEAPON || (proto->SubClass == ITEM_SUBCLASS_ARMOR_SHIELD ||
        //         (proto->SubClass == ITEM_SUBCLASS_ARMOR_MISC && proto->InventoryType == INVTYPE_HOLDABLE))) &&
        //             !ShouldEquipWeaponForSpec(clazz, spec, proto))
        //             continue;

        //         StatWeight statWeight;
        //         statWeight.id = m_weightScales[clazz][spec].info.id;
        //         uint32 statW = CalculateStatWeight(clazz, spec, proto);
        //         // set stat weight = 1 for items that can be equipped but have no proper stats
        //         statWeight.weight = statW ? statW : 1;
        //         //statWeight.weight = statW;
        //         // save item statWeight into ItemCache
        //         cacheInfo.weights[statWeight.id] = statWeight.weight;
        //         LOG_DEBUG("playerbots", "Item: {}, weight: {}, class: {}, spec: {}", proto->ItemId,
        //         statWeight.weight, clazz, m_weightScales[clazz][spec].info.name);
        //     }
        // }

        // cacheInfo.team = TEAM_NEUTRAL;

        // // check faction
        // if (proto->HasFlag2(ITEM_FLAG2_FACTION_HORDE))
        //     cacheInfo.team = TEAM_HORDE;

        // if (proto->HasFlag2(ITEM_FLAG2_FACTION_ALLIANCE))
        //     cacheInfo.team = TEAM_ALLIANCE;

        // if (cacheInfo.team == TEAM_NEUTRAL && proto->AllowableRace > 1 && proto->AllowableRace < 8388607)
        // {
        //     if (FactionEntry const* faction = sFactionStore.LookupEntry(HORDE))
        //         if ((proto->AllowableRace & faction->BaseRepRaceMask[0]) != 0)
        //             cacheInfo.team = TEAM_HORDE;

        //     if (FactionEntry const* faction = sFactionStore.LookupEntry(ALLIANCE))
        //         if ((proto->AllowableRace & faction->BaseRepRaceMask[0]) != 0)
        //             cacheInfo.team = TEAM_ALLIANCE;
        // }

        // if (cacheInfo.team < TEAM_NEUTRAL)
        //     LOG_DEBUG("playerbots", "Item: {}, team (item): {}", proto->ItemId, cacheInfo.team == TEAM_ALLIANCE ?
        //     "Alliance" : "Horde");

        // // check min level
        // if (proto->RequiredLevel)
        //     cacheInfo.minLevel = proto->RequiredLevel;

        // // check item source

        // if (proto->HasFlag(ITEM_FLAG_NO_DISENCHANT))
        // {
        //     cacheInfo.source = ITEM_SOURCE_PVP;
        //     LOG_DEBUG("playerbots", "Item: {}, source: PvP Reward", proto->ItemId);
        // }

        // // check quests
        // if (cacheInfo.source == ITEM_SOURCE_NONE)
        // {
        //     std::vector<uint32> questIds = GetQuestIdsForItem(proto->ItemId);
        //     if (questIds.size())
        //     {
        //         bool isAlly = false;
        //         bool isHorde = false;
        //         for (std::vector<uint32>::iterator i = questIds.begin(); i != questIds.end(); ++i)
        //         {
        //             Quest const* quest = sObjectMgr->GetQuestTemplate(*i);
        //             if (quest)
        //             {
        //                 cacheInfo.source = ITEM_SOURCE_QUEST;
        //                 cacheInfo.sourceId = *i;
        //                 if (!cacheInfo.minLevel)
        //                     cacheInfo.minLevel = quest->GetMinLevel();

        //                 // check quest team
        //                 if (cacheInfo.team == TEAM_NEUTRAL)
        //                 {
        //                     uint32 reqRace = quest->GetAllowableRaces();
        //                     if (reqRace)
        //                     {
        //                         if ((reqRace & RACEMASK_ALLIANCE) != 0)
        //                             isAlly = true;
        //                         else if ((reqRace & RACEMASK_HORDE) != 0)
        //                             isHorde = true;
        //                     }
        //                 }
        //             }
        //         }

        //         if (isAlly && isHorde)
        //             cacheInfo.team = TEAM_NEUTRAL;
        //         else if (isAlly)
        //             cacheInfo.team = TEAM_ALLIANCE;
        //         else if (isHorde)
        //             cacheInfo.team = TEAM_HORDE;

        //         LOG_DEBUG("playerbots", "Item: {}, team (quest): {}", proto->ItemId, cacheInfo.team == TEAM_ALLIANCE
        //         ? "Alliance" : cacheInfo.team == TEAM_HORDE ? "Horde" : "Both"); LOG_DEBUG("playerbots", "Item: {},
        //         source: quest {}, minlevel: {}", proto->ItemId, cacheInfo.sourceId, cacheInfo.minLevel);
        //     }
        // }

        // if (cacheInfo.minLevel)
        //     LOG_DEBUG("playerbots", "Item: {}, minlevel: {}", proto->ItemId, cacheInfo.minLevel);

        // // check vendors
        // if (cacheInfo.source == ITEM_SOURCE_NONE)
        // {
        //     for (std::set<uint32>::iterator i = vendorItems.begin(); i != vendorItems.end(); ++i)
        //     {
        //         if (proto->ItemId == *i)
        //         {
        //             cacheInfo.source = ITEM_SOURCE_VENDOR;
        //             LOG_DEBUG("playerbots", "Item: {} source: vendor", proto->ItemId);
        //             break;
        //         }
        //     }
        // }

        // // check drops
        // std::vector<int32> creatures;
        // std::vector<int32> gameobjects;
        // auto range = dropMap->equal_range(itr.first);

        // for (auto iter = range.first; iter != range.second; ++iter)
        // {
        //     if (iter->second > 0)
        //         creatures.push_back(iter->second);
        //     else
        //         gameobjects.push_back(abs(iter->second));
        // }

        // // check creature drop
        // if (cacheInfo.source == ITEM_SOURCE_NONE)
        // {
        //     if (creatures.size())
        //     {
        //         if (creatures.size() == 1)
        //         {
        //             cacheInfo.source = ITEM_SOURCE_DROP;
        //             cacheInfo.sourceId = creatures.front();
        //             LOG_DEBUG("playerbots", "Item: {}, source: creature drop, ID: {}", proto->ItemId,
        //             creatures.front());
        //         }
        //         else
        //         {
        //             cacheInfo.source = ITEM_SOURCE_DROP;
        //             LOG_DEBUG("playerbots", "Item: {}, source: creatures drop, number: {}", proto->ItemId,
        //             creatures.size());
        //         }
        //     }
        // }

        // // check gameobject drop
        // if (cacheInfo.source == ITEM_SOURCE_NONE || (cacheInfo.source == ITEM_SOURCE_DROP && !cacheInfo.sourceId))
        // {
        //     if (gameobjects.size())
        //     {
        //         if (gameobjects.size() == 1)
        //         {
        //             cacheInfo.source = ITEM_SOURCE_DROP;
        //             cacheInfo.sourceId = gameobjects.front();
        //             LOG_INFO("playerbots", "Item: {}, source: gameobject, ID: {}", proto->ItemId,
        //             gameobjects.front());
        //         }
        //         else
        //         {
        //             cacheInfo.source = ITEM_SOURCE_DROP;
        //             LOG_INFO("playerbots", "Item: {}, source: gameobjects, number: {}", proto->ItemId,
        //             gameobjects.size());
        //         }
        //     }
        // }

        // // check faction
        // if (proto->RequiredReputationFaction > 0 && proto->RequiredReputationFaction != 35 &&
        // proto->RequiredReputationRank < 15)
        // {
        //     cacheInfo.repFaction = proto->RequiredReputationFaction;
        //     cacheInfo.repRank = proto->RequiredReputationRank;
        // }

        // cacheInfo.quality = proto->Quality;
        // cacheInfo.itemId = proto->ItemId;
        // cacheInfo.slot = slot;

        // // save cache
        // PlayerbotsDatabasePreparedStatement* stmt =
        // PlayerbotsDatabase.GetPreparedStatement(PLAYERBOTS_DEL_EQUIP_CACHE_NEW); stmt->SetData(0, proto->ItemId);
        // trans->Append(stmt);

        // stmt = PlayerbotsDatabase.GetPreparedStatement(PLAYERBOTS_INS_EQUIP_CACHE_NEW);
        // stmt->SetData(0, cacheInfo.itemId);
        // stmt->SetData(1, cacheInfo.quality);
        // stmt->SetData(2, cacheInfo.slot);
        // stmt->SetData(3, cacheInfo.source);
        // stmt->SetData(4, cacheInfo.sourceId);
        // stmt->SetData(5, cacheInfo.team);
        // stmt->SetData(6, cacheInfo.repFaction);
        // stmt->SetData(7, cacheInfo.repRank);
        // stmt->SetData(8, cacheInfo.minLevel);

        // for (uint8 i = 1; i <= MAX_STAT_SCALES; ++i)
        // {
        //     if (cacheInfo.weights[i])
        //         stmt->SetData(8 + i, cacheInfo.weights[i]);
        //     else
        //         stmt->SetData(8 + i, 0);
        // }

        // trans->Append(stmt);

        // itemInfoCache[cacheInfo.itemId] = std::move(cacheInfo);
    }

    PlayerbotsDatabase.CommitTransaction(trans);
}

uint32 RandomItemMgr::CalculateStatWeight(uint8 playerclass, uint8 spec, ItemTemplate const* proto)
{
    uint32 statWeight = 0;
    bool isCasterItem = false;
    bool isAttackItem = false;
    bool noCaster = (Classes)playerclass == CLASS_WARRIOR || (Classes)playerclass == CLASS_ROGUE ||
                    (Classes)playerclass == CLASS_DEATH_KNIGHT || (Classes)playerclass == CLASS_HUNTER;
    uint32 spellPower = 0;
    uint32 spellHeal = 0;
    uint32 attackPower = 0;
    bool hasInt = false;
    bool hasMana = !((Classes)playerclass == CLASS_WARRIOR || (Classes)playerclass == CLASS_ROGUE ||
                     (Classes)playerclass == CLASS_DEATH_KNIGHT);

    if (proto->SubClass == ITEM_SUBCLASS_ARMOR_LIBRAM || proto->SubClass == ITEM_SUBCLASS_ARMOR_IDOL ||
        proto->SubClass == ITEM_SUBCLASS_ARMOR_TOTEM || proto->SubClass == ITEM_SUBCLASS_ARMOR_SIGIL)
        return (uint32)(proto->Quality + proto->ItemLevel);

    // check basic item stats
    int32 basicStatsWeight = 0;
    for (uint8 j = 0; j < MAX_ITEM_PROTO_STATS; ++j)
    {
        uint32 statType;
        int32 val;
        std::string weightName;

        // if (j >= proto->StatsCount)
        //     continue;

        statType = proto->ItemStat[j].ItemStatType;
        val = proto->ItemStat[j].ItemStatValue;

        if (val == 0)
            continue;

        for (std::map<std::string, uint32>::iterator i = weightStatLink.begin(); i != weightStatLink.end(); ++i)
        {
            uint32 modd = i->second;
            if (modd == statType)
            {
                weightName = i->first;
                break;
            }
        }

        if (weightName.empty())
            continue;

        uint32 singleStat = CalculateSingleStatWeight(playerclass, spec, weightName, val);
        basicStatsWeight += singleStat;

        if (val)
        {
            if (weightName == "int" && !noCaster)
                isCasterItem = true;

            if (weightName == "int")
                hasInt = true;

            if (weightName == "splpwr")
                isCasterItem = true;

            if (weightName == "str")
                isAttackItem = true;

            if (weightName == "agi")
                isAttackItem = true;

            if (weightName == "atkpwr")
                isAttackItem = true;
        }
    }

    // check armor & block
    statWeight += CalculateSingleStatWeight(playerclass, spec, "armor", proto->Armor);
    statWeight += CalculateSingleStatWeight(playerclass, spec, "block", proto->Block);

    // check weapon dps
    if (proto->IsWeaponVellum())
    {
        //WeaponAttackType attType = BASE_ATTACK; //not used, line marked for removal.

        uint32 dps = 0;
        for (uint8 i = 0; i < MAX_ITEM_PROTO_DAMAGES; i++)
        {
            if (proto->Damage[i].DamageMax == 0)
                break;

            dps = (proto->Damage[i].DamageMin + proto->Damage[i].DamageMax) / (float)(proto->Delay / 1000.0f) / 2;
            if (dps)
            {
                if (proto->IsRangedWeapon())
                    statWeight += CalculateSingleStatWeight(playerclass, spec, "rgddps", dps);
                else
                    statWeight += CalculateSingleStatWeight(playerclass, spec, "mledps", dps);
            }
        }
    }

    // check item spells
    for (auto const& spellData : proto->Spells)
    {
        // no spell
        if (!spellData.SpellId)
            continue;

        // apply only at-equip spells
        if (spellData.SpellTrigger != ITEM_SPELLTRIGGER_ON_EQUIP)
            continue;

        // check if it is valid spell
        SpellInfo const* spellInfo = sSpellMgr->GetSpellInfo(spellData.SpellId);
        if (!spellInfo)
            continue;

        uint32 spellDamage = 0;
        uint32 spellHealing = 0;
        for (uint32 j = 0; j < MAX_SPELL_EFFECTS; j++)
        {
            if (spellInfo->Effects[j].Effect == SPELL_EFFECT_APPLY_AURA && spellInfo->Effects[j].BasePoints)
            {
                // spell damage
                // SPELL_AURA_MOD_DAMAGE_DONE
                if (spellInfo->Effects[j].ApplyAuraName == SPELL_AURA_MOD_DAMAGE_DONE)
                {
                    spellDamage = spellInfo->Effects[j].BasePoints + 1;
                }

                // spell healing
                // SPELL_AURA_MOD_HEALING_DONE
                if (spellInfo->Effects[j].ApplyAuraName == SPELL_AURA_MOD_HEALING_DONE)
                {
                    spellHealing = spellInfo->Effects[j].BasePoints + 1;
                }

                // check spell power
                if (spellDamage && spellDamage == spellHealing)
                {
                    isCasterItem = true;
                    spellPower += CalculateSingleStatWeight(playerclass, spec, "splpwr", spellDamage);
                }

                // spell hit rating (pre tbc)
                // SPELL_AURA_MOD_SPELL_HIT_CHANCE
                if (spellInfo->Effects[j].ApplyAuraName == SPELL_AURA_MOD_SPELL_HIT_CHANCE)
                {
                    statWeight += CalculateSingleStatWeight(playerclass, spec, "spellhitrtng",
                                                            spellInfo->Effects[j].BasePoints + 1);
                }

                // spell crit rating (pre tbc)
                // SPELL_AURA_MOD_SPELL_CRIT_CHANCE
                if (spellInfo->Effects[j].ApplyAuraName == SPELL_AURA_MOD_SPELL_CRIT_CHANCE)
                {
                    statWeight += CalculateSingleStatWeight(playerclass, spec, "spellcritstrkrtng",
                                                            spellInfo->Effects[j].BasePoints + 1);
                }

                // spell penetration
                // SPELL_AURA_MOD_TARGET_RESISTANCE
                if (spellInfo->Effects[j].ApplyAuraName == SPELL_AURA_MOD_TARGET_RESISTANCE)
                {
                    // check if magic type
                    if (spellInfo->Effects[j].MiscValue == SPELL_SCHOOL_MASK_SPELL)
                        statWeight += CalculateSingleStatWeight(playerclass, spec, "spellpenrtng",
                                                                abs(spellInfo->Effects[j].BasePoints + 1));
                }

                // check attack power
                if (spellInfo->Effects[j].ApplyAuraName == SPELL_AURA_MOD_ATTACK_POWER)
                {
                    isAttackItem = true;
                    attackPower +=
                        CalculateSingleStatWeight(playerclass, spec, "atkpwr", spellInfo->Effects[j].BasePoints + 1);
                }

                // check ranged ap
                // SPELL_AURA_MOD_RANGED_ATTACK_POWER
                if (spellInfo->Effects[j].ApplyAuraName == SPELL_AURA_MOD_RANGED_ATTACK_POWER)
                {
                    isAttackItem = true;
                    attackPower +=
                        CalculateSingleStatWeight(playerclass, spec, "atkpwr", spellInfo->Effects[j].BasePoints + 1);
                }

                // check block
                if (spellInfo->Effects[j].ApplyAuraName == SPELL_AURA_MOD_SHIELD_BLOCKVALUE)
                {
                    statWeight +=
                        CalculateSingleStatWeight(playerclass, spec, "block", spellInfo->Effects[j].BasePoints + 1);
                }

                // block chance
                // SPELL_AURA_MOD_BLOCK_PERCENT
                if (spellInfo->Effects[j].ApplyAuraName == SPELL_AURA_MOD_BLOCK_PERCENT)
                {
                    statWeight +=
                        CalculateSingleStatWeight(playerclass, spec, "blockrtng", spellInfo->Effects[j].BasePoints + 1);
                }

                // armor penetration
                // SPELL_AURA_MOD_TARGET_RESISTANCE
                if (spellInfo->Effects[j].ApplyAuraName == SPELL_AURA_MOD_TARGET_RESISTANCE)
                {
                    // check if physical type
                    if (spellInfo->Effects[j].MiscValue == SPELL_SCHOOL_MASK_NORMAL)
                        statWeight += CalculateSingleStatWeight(playerclass, spec, "armorpenrtng",
                                                                abs(spellInfo->Effects[j].BasePoints + 1));
                }

                // hit rating (pre tbc)
                // SPELL_AURA_MOD_HIT_CHANCE
                if (spellInfo->Effects[j].ApplyAuraName == SPELL_AURA_MOD_HIT_CHANCE)
                {
                    statWeight +=
                        CalculateSingleStatWeight(playerclass, spec, "hitrtng", spellInfo->Effects[j].BasePoints + 1);
                }

                // crit rating (pre tbc)
                // SPELL_AURA_MOD_HIT_CHANCE
                if (spellInfo->Effects[j].ApplyAuraName == SPELL_AURA_MOD_CRIT_PCT)
                {
                    statWeight += CalculateSingleStatWeight(playerclass, spec, "critstrkrtng",
                                                            spellInfo->Effects[j].BasePoints + 1);
                }

                // check defense SPELL_AURA_MOD_SKILL
                // check block
                // if (spellInfo->Effects[j].ApplyAuraName == SPELL_AURA_MOD_SKILL)
                //{
                //    statWeight += CalculateSingleStatWeight(playerclass, spec, "block",
                //    spellInfo->Effects[j].BasePoints + 1);
                //}

                // ratings
                // enum CombatRating
                if (spellInfo->Effects[j].ApplyAuraName == SPELL_AURA_MOD_RATING)
                {
                    for (uint32 rating = 0; rating < MAX_COMBAT_RATING; ++rating)
                    {
                        if (spellInfo->Effects[j].MiscValue & (1 << rating))
                        {
                            int32 val = spellInfo->Effects[j].BasePoints + 1;
                            std::string weightName;

                            for (std::map<std::string, uint32>::iterator i = weightRatingLink.begin();
                                 i != weightRatingLink.end(); ++i)
                            {
                                uint32 modd = i->second;
                                if (modd == rating)
                                {
                                    weightName = i->first;
                                    break;
                                }
                            }

                            if (weightName.empty())
                                continue;

                            statWeight += CalculateSingleStatWeight(playerclass, spec, weightName, val);
                        }
                    }
                }

                // mana regen
                // SPELL_AURA_MOD_POWER_REGEN
                if (spellInfo->Effects[j].ApplyAuraName == SPELL_AURA_MOD_POWER_REGEN)
                {
                    statWeight +=
                        CalculateSingleStatWeight(playerclass, spec, "manargn", spellInfo->Effects[j].BasePoints + 1);
                }
            }
        }
    }

    // check for caster item
    if (isCasterItem || hasInt)
    {
        if ((!hasMana || noCaster) && spellPower)
            return 0;

        if (!hasMana && hasInt)
            return 0;

        if ((!hasMana && noCaster && playerclass != CLASS_PALADIN) && spellPower > attackPower)
            return 0;

        bool playerCaster = false;
        for (std::vector<WeightScaleStat>::iterator i = m_weightScales[playerclass][spec].stats.begin();
             i != m_weightScales[playerclass][spec].stats.end(); ++i)
        {
            if (i->stat == "splpwr" || i->stat == "int" || i->stat == "manargn")
            {
                playerCaster = true;
            }
        }

        if (!playerCaster)
            return 0;
    }

    // check for caster item
    if (isAttackItem)
    {
        if (hasMana && !noCaster && !(hasInt || spellPower))
            return 0;
        // if (!noCaster && attackPower)
        //     return 0;

        bool playerAttacker = false;
        for (std::vector<WeightScaleStat>::iterator i = m_weightScales[playerclass][spec].stats.begin();
             i != m_weightScales[playerclass][spec].stats.end(); ++i)
        {
            if (i->stat == "str" || i->stat == "agi" || i->stat == "atkpwr" || i->stat == "mledps" ||
                i->stat == "rgddps")
            {
                playerAttacker = true;
            }
        }

        if (!playerAttacker)
            return 0;
    }

    statWeight += spellPower;
    statWeight += spellHeal;
    statWeight += attackPower;

    // handle negative stats
    if (basicStatsWeight < 0 && (abs(basicStatsWeight) >= statWeight))
        statWeight = 0;
    else
        statWeight += basicStatsWeight;

    return statWeight;
}

uint32 RandomItemMgr::CalculateSingleStatWeight(uint8 playerclass, uint8 spec, std::string stat, uint32 value)
{
    uint32 statWeight = 0;
    for (std::vector<WeightScaleStat>::iterator i = m_weightScales[playerclass][spec].stats.begin();
         i != m_weightScales[playerclass][spec].stats.end(); ++i)
    {
        if (stat == i->stat)
        {
            statWeight = i->weight * value;
            // if (statWeight)
            //     LOG_INFO("playerbots", "stat: {}, val: {}, weight: {}, total: {}, class: {}, spec: {}",
            //         stat, value, i->weight, statWeight, playerclass, m_weightScales[playerclass][spec].info.name);
            return statWeight;
        }
    }

    return statWeight;
}

uint32 RandomItemMgr::GetQuestIdForItem(uint32 itemId)
{
    bool isQuest = false;
    uint32 questId = 0;
    ObjectMgr::QuestMap const& questTemplates = sObjectMgr->GetQuestTemplates();
    for (ObjectMgr::QuestMap::const_iterator i = questTemplates.begin(); i != questTemplates.end(); ++i)
    {
        Quest const* quest = i->second;

        uint32 rewItemCount = quest->GetRewItemsCount();
        for (uint32 i = 0; i < rewItemCount; ++i)
        {
            if (!quest->RewardItemId[i])
                continue;

            if (quest->RewardItemId[i] == itemId)
            {
                isQuest = true;
                questId = quest->GetQuestId();
                break;
            }
        }

        uint32 rewChocieItemCount = quest->GetRewChoiceItemsCount();
        for (uint32 i = 0; i < rewChocieItemCount; ++i)
        {
            if (!quest->RewardChoiceItemId[i])
                continue;

            if (quest->RewardChoiceItemId[i] == itemId)
            {
                isQuest = true;
                questId = quest->GetQuestId();
                break;
            }
        }
        if (isQuest)
            break;
    }
    return questId;
}

std::vector<uint32> RandomItemMgr::GetQuestIdsForItem(uint32 itemId)
{
    std::vector<uint32> questIds;
    ObjectMgr::QuestMap const& questTemplates = sObjectMgr->GetQuestTemplates();
    for (ObjectMgr::QuestMap::const_iterator i = questTemplates.begin(); i != questTemplates.end(); ++i)
    {
        Quest const* quest = i->second;

        uint32 rewItemCount = quest->GetRewItemsCount();
        for (uint32 i = 0; i < rewItemCount; ++i)
        {
            if (!quest->RewardItemId[i])
                continue;

            if (quest->RewardItemId[i] == itemId)
            {
                questIds.push_back(quest->GetQuestId());
                break;
            }
        }

        uint32 rewChocieItemCount = quest->GetRewChoiceItemsCount();
        for (uint32 i = 0; i < rewChocieItemCount; ++i)
        {
            if (!quest->RewardChoiceItemId[i])
                continue;

            if (quest->RewardChoiceItemId[i] == itemId)
            {
                questIds.push_back(quest->GetQuestId());
                break;
            }
        }
    }

    return questIds;
}

uint32 RandomItemMgr::GetUpgrade(Player* player, std::string spec, uint8 slot, uint32 quality, uint32 itemId)
{
    if (!player)
        return 0;

    // get old item statWeight
    uint32 oldStatWeight = 0;
    uint32 specId = 0;
    uint32 closestUpgrade = 0;
    uint32 closestUpgradeWeight = 0;
    std::vector<uint32> classspecs;

    for (uint32 specNum = 1; specNum < 5; ++specNum)
    {
        if (!m_weightScales[player->getClass()][specNum].info.id)
            continue;

        classspecs.push_back(m_weightScales[player->getClass()][specNum].info.id);

        if (m_weightScales[player->getClass()][specNum].info.name == spec)
            specId = m_weightScales[player->getClass()][specNum].info.id;
    }
    if (!specId)
        return 0;

    if (itemId && itemInfoCache.find(itemId) != itemInfoCache.end())
    {
        oldStatWeight = itemInfoCache[itemId].weights[specId];

        if (oldStatWeight)
            LOG_INFO("playerbots", "Old Item: {}, weight: {}", itemId, oldStatWeight);
        else
            LOG_INFO("playerbots", "Old item has no stat weight");
    }

    for (std::map<uint32, ItemInfoEntry>::iterator i = itemInfoCache.begin(); i != itemInfoCache.end(); ++i)
    {
        ItemInfoEntry& info = i->second;

        // skip useless items
        if (info.weights[specId] == 0)
            continue;

        // skip higher lvl
        if (info.minLevel > player->GetLevel())
            continue;

        // skip too low level
        if (info.minLevel < (player->GetLevel() - 10))
            continue;

        // skip wrong team
        if (info.team != TEAM_NEUTRAL && info.team != player->GetTeamId())
            continue;

        // skip wrong slot
        if ((EquipmentSlots)info.slot != (EquipmentSlots)slot)
            continue;

        // skip higher quality
        if (quality && info.quality != quality)
            continue;

        // skip worse items
        if (info.weights[specId] <= oldStatWeight)
            continue;

        // skip items that only fit in slot, but not stats
        if (!itemId && info.weights[specId] == 1 && player->GetLevel() > 40)
            continue;

        // skip quest items
        if (info.source == ITEM_SOURCE_QUEST)
        {
            if (player->GetQuestRewardStatus(info.sourceId) != QUEST_STATUS_COMPLETE)
                continue;
        }

        // skip no stats trinkets
        if (info.weights[specId] == 1 && info.slot == EQUIPMENT_SLOT_NECK || info.slot == EQUIPMENT_SLOT_TRINKET1 ||
            info.slot == EQUIPMENT_SLOT_TRINKET2 || info.slot == EQUIPMENT_SLOT_FINGER1 ||
            info.slot == EQUIPMENT_SLOT_FINGER2)
            continue;

        // check if item stat score is the best among class specs
        uint32 bestSpecId = 0;
        uint32 bestSpecScore = 0;
        for (std::vector<uint32>::iterator i = classspecs.begin(); i != classspecs.end(); ++i)
        {
            if (info.weights[*i] > bestSpecScore)
            {
                bestSpecId = *i;
                bestSpecScore = info.weights[specId];
            }
        }

        if (bestSpecId && bestSpecId != specId && player->GetLevel() > 40)
            return 0;

        if (!closestUpgrade)
        {
            closestUpgrade = info.itemId;
            closestUpgradeWeight = info.weights[specId];
        }

        // pick closest upgrade
        if (info.weights[specId] < closestUpgradeWeight)
        {
            closestUpgrade = info.itemId;
            closestUpgradeWeight = info.weights[specId];
        }
    }

    if (closestUpgrade)
        LOG_INFO("playerbots", "New Item: {}, weight: {}", closestUpgrade, closestUpgradeWeight);

    return closestUpgrade;
}

std::vector<uint32> RandomItemMgr::GetUpgradeList(Player* player, std::string spec, uint8 slot, uint32 quality,
                                                  uint32 itemId, uint32 /*amount*/)
{
    std::vector<uint32> listItems;
    if (!player)
        return listItems;

    // get old item statWeight
    uint32 oldStatWeight = 0;
    uint32 specId = 0;
    uint32 closestUpgradeWeight = 0;
    std::vector<uint32> classspecs;

    for (uint32 specNum = 1; specNum < 5; ++specNum)
    {
        if (!m_weightScales[player->getClass()][specNum].info.id)
            continue;

        classspecs.push_back(m_weightScales[player->getClass()][specNum].info.id);

        if (m_weightScales[player->getClass()][specNum].info.name == spec)
            specId = m_weightScales[player->getClass()][specNum].info.id;
    }

    if (!specId)
        return listItems;

    if (itemId && itemInfoCache.find(itemId) != itemInfoCache.end())
    {
        oldStatWeight = itemInfoCache[itemId].weights[specId];

        if (oldStatWeight)
            LOG_INFO("playerbots", "Old Item: {}, weight: {}", itemId, oldStatWeight);
        else
            LOG_INFO("playerbots", "Old item has no stat weight");
    }

    for (std::map<uint32, ItemInfoEntry>::iterator i = itemInfoCache.begin(); i != itemInfoCache.end(); ++i)
    {
        ItemInfoEntry& info = i->second;

        // skip useless items
        if (info.weights[specId] == 0)
            continue;

        // skip higher lvl
        if (info.minLevel > player->GetLevel())
            continue;

        // skip too low level
        if ((int32)info.minLevel < (int32)(player->GetLevel() - 20))
            continue;

        // skip wrong team
        if (info.team != TEAM_NEUTRAL && info.team != player->GetTeamId())
            continue;

        // skip wrong slot
        if ((EquipmentSlots)info.slot != (EquipmentSlots)slot)
            continue;

        // skip higher quality
        if (quality && info.quality != quality)
            continue;

        // skip worse items
        if (info.weights[specId] <= oldStatWeight)
            continue;

        // skip items that only fit in slot, but not stats
        if (!itemId && info.weights[specId] == 1 && player->GetLevel() > 40)
            continue;

        // skip quest items
        if (info.source == ITEM_SOURCE_QUEST)
        {
            if (player->GetQuestRewardStatus(info.sourceId) != QUEST_STATUS_COMPLETE)
                continue;
        }

        // skip no stats trinkets
        if (info.weights[specId] < 2 && (info.slot == EQUIPMENT_SLOT_NECK || info.slot == EQUIPMENT_SLOT_TRINKET1 ||
                                         info.slot == EQUIPMENT_SLOT_TRINKET2 || info.slot == EQUIPMENT_SLOT_FINGER1 ||
                                         info.slot == EQUIPMENT_SLOT_FINGER2))
            continue;

        // if (player->GetLevel() >= 40)
        //{
        //     // check if item stat score is the best among class specs
        //     uint32 bestSpecId = 0;
        //     uint32 bestSpecScore = 0;
        //     for (vector<uint32>::iterator i = classspecs.begin(); i != classspecs.end(); ++i)
        //     {
        //         if (info->weights[*i] > bestSpecScore)
        //         {
        //             bestSpecId = *i;
        //             bestSpecScore = info->weights[specId];
        //         }
        //     }

        //    if (bestSpecId && bestSpecId != specId)
        //        continue;
        //}

        listItems.push_back(info.itemId);
        // continue;

        // pick closest upgrade
        if (info.weights[specId] > closestUpgradeWeight)
        {
            closestUpgradeWeight = info.weights[specId];
        }
    }

    if (listItems.size())
        LOG_INFO("playerbots", "New Items: {}, Old item:%d, New items max: {}", listItems.size(), oldStatWeight,
                 closestUpgradeWeight);

    return listItems;
}

bool RandomItemMgr::HasStatWeight(uint32 itemId)
{
    auto itr = itemInfoCache.find(itemId);
    return itr != itemInfoCache.end();
}

uint32 RandomItemMgr::GetMinLevelFromCache(uint32 itemId)
{
    auto itr = itemInfoCache.find(itemId);
    if (itr == itemInfoCache.end())
        return 0;

    return itr->second.minLevel;
}

uint32 RandomItemMgr::GetStatWeight(Player* player, uint32 itemId)
{
    if (!player || !itemId)
        return 0;

    auto itr = itemInfoCache.find(itemId);
    if (itr == itemInfoCache.end())
        return 0;

    uint32 statWeight = 0;
    uint32 specId = 0;
    std::vector<uint32> classspecs;

    std::string const& specName = AiFactory::GetPlayerSpecName(player);
    if (specName.empty())
        return 0;

    for (uint32 specNum = 1; specNum < 5; ++specNum)
    {
        if (!m_weightScales[player->getClass()][specNum].info.id)
            continue;

        classspecs.push_back(m_weightScales[player->getClass()][specNum].info.id);

        if (m_weightScales[player->getClass()][specNum].info.name == specName)
            specId = m_weightScales[player->getClass()][specNum].info.id;

        if (m_weightScales[player->getClass()][specNum].info.name == specName)
        {
            specId = specNum;
            break;
        }
    }

    if (!specId)
        return 0;

    statWeight = itr->second.weights[specId];

    return statWeight;
}

uint32 RandomItemMgr::GetLiveStatWeight(Player* player, uint32 itemId)
{
    if (!player || !itemId)
        return 0;

    auto itr = itemInfoCache.find(itemId);
    if (itr == itemInfoCache.end())
        return 0;

    uint32 statWeight = 0;
    uint32 specId = 0;
    std::vector<uint32> classspecs;

    std::string const& specName = AiFactory::GetPlayerSpecName(player);
    if (specName.empty())
        return 0;

    for (uint32 specNum = 1; specNum < 5; ++specNum)
    {
        if (!m_weightScales[player->getClass()][specNum].info.id)
            continue;

        // for bestSpec check
        // classspecs.push_back(m_weightScales[player->getClass()][specNum].info.id);

        if (m_weightScales[player->getClass()][specNum].info.name == specName)
            specId = m_weightScales[player->getClass()][specNum].info.id;
    }
    if (!specId)
        return 0;

    statWeight = itr->second.weights[specId];

    // skip higher lvl
    if (itr->second.minLevel > player->GetLevel())
        return 0;

    // skip too low level
    // if ((int32)info->minLevel < (int32)(player->GetLevel() - 20))
    //    return 0;

    // skip wrong team
    if (itr->second.team != TEAM_NEUTRAL && itr->second.team != player->GetTeamId())
        return 0;

    // skip quest items
    if (itr->second.source == ITEM_SOURCE_QUEST && itr->second.sourceId)
    {
        if (player->GetQuestRewardStatus(itr->second.sourceId) != QUEST_STATUS_COMPLETE)
            return 0;
    }

    // skip pvp items
    if (itr->second.source == ITEM_SOURCE_PVP)
    {
        if (!player->GetHonorPoints() && !player->GetArenaPoints())
            return 0;
    }

    // skip no stats trinkets
    if (itr->second.weights[specId] == 1 &&
        (itr->second.slot == EQUIPMENT_SLOT_NECK || itr->second.slot == EQUIPMENT_SLOT_TRINKET1 ||
         itr->second.slot == EQUIPMENT_SLOT_TRINKET2 || itr->second.slot == EQUIPMENT_SLOT_FINGER1 ||
         itr->second.slot == EQUIPMENT_SLOT_FINGER2))
        return 0;

    // skip items that only fit in slot, but not stats
    if (!itemId && itr->second.weights[specId] == 1 && player->GetLevel() > 20)
        return 0;

    // check if item stat score is the best among class specs
    /*uint32 bestSpecId = 0;
    uint32 bestSpecScore = 0;
    for (vector<uint32>::iterator i = classspecs.begin(); i != classspecs.end(); ++i)
    {
        if (itemCache[itemId]->weights[*i] > bestSpecScore && itemCache[itemId]->weights[*i] > 1)
        {
            bestSpecId = *i;
            bestSpecScore = itemCache[itemId]->weights[specId];
        }
    }

    if (bestSpecId && bestSpecId != specId && player->GetLevel() >= 60)
        return 0;*/

    return statWeight;
}

void RandomItemMgr::BuildEquipCache()
{
    uint32 maxLevel = sPlayerbotAIConfig.randomBotMaxLevel;
    if (maxLevel > sWorld->getIntConfig(CONFIG_MAX_PLAYER_LEVEL))
        maxLevel = sWorld->getIntConfig(CONFIG_MAX_PLAYER_LEVEL);

    ItemTemplateContainer const* itemTemplates = sObjectMgr->GetItemTemplateStore();

    PlayerbotsDatabasePreparedStatement* stmt = PlayerbotsDatabase.GetPreparedStatement(PLAYERBOTS_SEL_EQUIP_CACHE);
    if (PreparedQueryResult result = PlayerbotsDatabase.Query(stmt))
    {
        LOG_INFO("server.loading",
                 "Loading equipment cache for {} classes, {} levels, {} slots, {} quality from {} items", MAX_CLASSES,
                 maxLevel, EQUIPMENT_SLOT_END, ITEM_QUALITY_ARTIFACT, itemTemplates->size());

        uint32 count = 0;
        do
        {
            Field* fields = result->Fetch();
            uint8 clazz = fields[0].Get<uint8>();
            uint32 level = fields[1].Get<uint32>();
            uint8 slot = fields[2].Get<uint8>();
            uint32 quality = fields[3].Get<uint32>();
            uint32 itemId = fields[4].Get<uint32>();

            BotEquipKey key(level, clazz, slot, quality);
            equipCache[key].push_back(itemId);
            ++count;
        } while (result->NextRow());

        LOG_INFO("playerbots", "Equipment cache loaded from {} records", count);
    }
    else
    {
        uint64 total = MAX_CLASSES * maxLevel * EQUIPMENT_SLOT_END * ITEM_QUALITY_ARTIFACT;
        LOG_INFO("server.loading",
                 "Building equipment cache for {} classes, {} levels, {} slots, {} quality from {} items ({} total)",
                 MAX_CLASSES, maxLevel, EQUIPMENT_SLOT_END, ITEM_QUALITY_ARTIFACT, itemTemplates->size(), total);

        for (uint8 class_ = CLASS_WARRIOR; class_ < MAX_CLASSES; ++class_)
        {
            // skip nonexistent classes
            if (!((1 << (class_ - 1)) & CLASSMASK_ALL_PLAYABLE) || !sChrClassesStore.LookupEntry(class_))
                continue;

            for (uint32 level = 1; level <= maxLevel; ++level)
            {
                for (uint8 slot = 0; slot < EQUIPMENT_SLOT_END; ++slot)
                {
                    for (uint32 quality = ITEM_QUALITY_POOR; quality <= ITEM_QUALITY_ARTIFACT; ++quality)
                    {
                        BotEquipKey key(level, class_, slot, quality);

                        RandomItemList items;
                        for (auto const& itr : *itemTemplates)
                        {
                            ItemTemplate const* proto = &itr.second;
                            if (!proto)
                                continue;

                            if (proto->Class != ITEM_CLASS_WEAPON && proto->Class != ITEM_CLASS_ARMOR &&
                                proto->Class != ITEM_CLASS_CONTAINER && proto->Class != ITEM_CLASS_PROJECTILE)
                                continue;

                            if (!CanEquipItem(key, proto))
                                continue;

                            if (proto->Class == ITEM_CLASS_ARMOR &&
                                (slot == EQUIPMENT_SLOT_HEAD || slot == EQUIPMENT_SLOT_SHOULDERS ||
                                 slot == EQUIPMENT_SLOT_CHEST || slot == EQUIPMENT_SLOT_WAIST ||
                                 slot == EQUIPMENT_SLOT_LEGS || slot == EQUIPMENT_SLOT_FEET ||
                                 slot == EQUIPMENT_SLOT_WRISTS || slot == EQUIPMENT_SLOT_HANDS) &&
                                !CanEquipArmor(key.clazz, key.level, proto))
                                continue;

                            if (proto->Class == ITEM_CLASS_WEAPON && !CanEquipWeapon(key.clazz, proto))
                                continue;

                            if (slot == EQUIPMENT_SLOT_OFFHAND && key.clazz == CLASS_ROGUE &&
                                proto->Class != ITEM_CLASS_WEAPON)
                                continue;

                            items.push_back(itr.first);

                            PlayerbotsDatabasePreparedStatement* stmt =
                                PlayerbotsDatabase.GetPreparedStatement(PLAYERBOTS_INS_EQUIP_CACHE);
                            stmt->SetData(0, class_);
                            stmt->SetData(1, level);
                            stmt->SetData(2, slot);
                            stmt->SetData(3, quality);
                            stmt->SetData(4, proto->ItemId);
                            PlayerbotsDatabase.Execute(stmt);
                        }

                        equipCache[key] = items;

                        LOG_DEBUG("playerbots",
                                  "Equipment cache for class: {}, level {}, slot {}, quality {}: {} items", class_,
                                  level, slot, quality, items.size());
                    }
                }
            }
        }

        LOG_INFO("server.loading", "Equipment cache saved to DB");
    }
}

void RandomItemMgr::BuildEquipCacheNew()
{
    LOG_INFO("playerbots", "Loading equipments cache...");

    std::unordered_set<uint32> questItemIds;
    ObjectMgr::QuestMap const& questTemplates = sObjectMgr->GetQuestTemplates();
    for (ObjectMgr::QuestMap::const_iterator i = questTemplates.begin(); i != questTemplates.end(); ++i)
    {
        //uint32 questId = i->first; //not used in this scope, line marked for removal.
        Quest const* quest = i->second;

        if (quest->IsRepeatable())
            continue;

        if (quest->GetQuestLevel() <= 0)
            continue;

        if (quest->GetRequiredClasses())
            continue;

        for (int j = 0; j < quest->GetRewChoiceItemsCount(); j++)
            if (uint32 itemId = quest->RewardChoiceItemId[j])
            {
                ItemTemplate const* proto = sObjectMgr->GetItemTemplate(itemId);
                if (proto->Class != ITEM_CLASS_WEAPON && proto->Class != ITEM_CLASS_ARMOR)
                    continue;
                int requiredLevel = std::max((int)proto->RequiredLevel, quest->GetQuestLevel());
                equipCacheNew[requiredLevel][proto->InventoryType].push_back(itemId);
                questItemIds.insert(itemId);
            }

        for (int j = 0; j < quest->GetRewItemsCount(); j++)
            if (uint32 itemId = quest->RewardItemId[j])
            {
                ItemTemplate const* proto = sObjectMgr->GetItemTemplate(itemId);
                if (proto->Class != ITEM_CLASS_WEAPON && proto->Class != ITEM_CLASS_ARMOR)
                    continue;
                int requiredLevel = std::max((int)proto->RequiredLevel, quest->GetQuestLevel());
                equipCacheNew[requiredLevel][proto->InventoryType].push_back(itemId);
                questItemIds.insert(itemId);
            }
    }

    ItemTemplateContainer const* itemTemplates = sObjectMgr->GetItemTemplateStore();
    for (auto const& itr : *itemTemplates)
    {
        ItemTemplate const* proto = &itr.second;
        if (!proto)
            continue;
        uint32 itemId = proto->ItemId;

        if (questItemIds.find(itemId) != questItemIds.end())
            continue;

        if (IsTestItem(itemId))
        {
            continue;
        }

        if (sPlayerbotAIConfig.unobtainableItems.find(itemId) != sPlayerbotAIConfig.unobtainableItems.end())
            continue;

        equipCacheNew[proto->RequiredLevel][proto->InventoryType].push_back(itemId);
    }
}

RandomItemList RandomItemMgr::Query(uint32 level, uint8 clazz, uint8 slot, uint32 quality)
{
    // return equipCache[key];
    BotEquipKey key(level, clazz, slot, quality);
    RandomItemList items;
    ItemTemplateContainer const* itemTemplates = sObjectMgr->GetItemTemplateStore();
    for (auto const& itr : *itemTemplates)
    {
        ItemTemplate const* proto = &itr.second;
        if (!proto)
            continue;

        if (proto->Class != ITEM_CLASS_WEAPON && proto->Class != ITEM_CLASS_ARMOR &&
            proto->Class != ITEM_CLASS_CONTAINER && proto->Class != ITEM_CLASS_PROJECTILE)
            continue;

        if (!CanEquipItem(key, proto))
            continue;

        if (proto->Class == ITEM_CLASS_ARMOR &&
            (slot == EQUIPMENT_SLOT_HEAD || slot == EQUIPMENT_SLOT_SHOULDERS || slot == EQUIPMENT_SLOT_CHEST ||
             slot == EQUIPMENT_SLOT_WAIST || slot == EQUIPMENT_SLOT_LEGS || slot == EQUIPMENT_SLOT_FEET ||
             slot == EQUIPMENT_SLOT_WRISTS || slot == EQUIPMENT_SLOT_HANDS) &&
            !CanEquipArmor(key.clazz, key.level, proto))
            continue;

        if (proto->Class == ITEM_CLASS_WEAPON && !CanEquipWeapon(key.clazz, proto))
            continue;

        if (slot == EQUIPMENT_SLOT_OFFHAND && key.clazz == CLASS_ROGUE && proto->Class != ITEM_CLASS_WEAPON)
            continue;

        items.push_back(itr.first);
    }
    return items;
}

void RandomItemMgr::BuildAmmoCache()
{
    uint32 maxLevel = sWorld->getIntConfig(CONFIG_MAX_PLAYER_LEVEL);

    LOG_INFO("server.loading", "Building ammo cache for {} levels", maxLevel);

    uint32 counter = 0;
    for (uint32 level = 1; level <= maxLevel; level += 1)
    {
        for (uint32 subClass = ITEM_SUBCLASS_ARROW; subClass <= ITEM_SUBCLASS_BULLET; subClass++)
        {
            QueryResult results = WorldDatabase.Query(
                "SELECT entry FROM item_template WHERE class = {} AND subclass = {} AND RequiredLevel <= {} AND duration = 0 "
                "AND (Flags & 16) = 0 AND dmg_min1 != 0 AND RequiredLevel != 0  "
                "ORDER BY stackable DESC, ItemLevel DESC",
                ITEM_CLASS_PROJECTILE, subClass, level);
            if (!results)
                continue;
            do
            {
                Field* fields = results->Fetch();
                uint32 entry = fields[0].Get<uint32>();
                ammoCache[level][subClass].push_back(entry);
                ++counter;
            } while (results->NextRow());
        }
    }

    LOG_INFO("server.loading", "Cached {} ammo", counter);  // TEST
}

std::vector<uint32> RandomItemMgr::GetAmmo(uint32 level, uint32 subClass) { return ammoCache[level][subClass]; }

void RandomItemMgr::BuildPotionCache()
{
    uint32 maxLevel = sWorld->getIntConfig(CONFIG_MAX_PLAYER_LEVEL);

    LOG_INFO("playerbots", "Building potion cache for {} levels", maxLevel);

    ItemTemplateContainer const* itemTemplates = sObjectMgr->GetItemTemplateStore();

    uint32 counter = 0;
    for (uint32 level = 1; level <= maxLevel; level++)
    {
        uint32 effects[] = {SPELL_EFFECT_HEAL, SPELL_EFFECT_ENERGIZE};
        for (uint8 i = 0; i < 2; ++i)
        {
            uint32 effect = effects[i];

            for (auto const& itr : *itemTemplates)
            {
                ItemTemplate const* proto = &itr.second;
                if (!proto)
                    continue;

                if (proto->Class != ITEM_CLASS_CONSUMABLE ||
                    (proto->SubClass != ITEM_SUBCLASS_POTION && proto->SubClass != ITEM_SUBCLASS_FLASK) ||
                    proto->Bonding != NO_BIND)
                    continue;

                uint32 requiredLevel = proto->RequiredLevel;
                if (requiredLevel > level || (level > 13 && requiredLevel < level - 13))
                    continue;

                if (proto->RequiredSkill)
                    continue;

                if (proto->Area || proto->Map || proto->RequiredCityRank || proto->RequiredHonorRank)
                    continue;

                if (proto->Duration & 0x80000000)
                    continue;

                if (proto->AllowableClass != -1)
                    continue;

                bool hybrid = false;
                SpellInfo const* spellInfo = sSpellMgr->GetSpellInfo(proto->Spells[0].SpellId);
                if (!spellInfo)
                    continue;
                // do not accept hybrid potion
                for (uint8 i = 1; i < 3; i++)
                {
                    if (spellInfo->Effects[i].Effect != 0)
                    {
                        hybrid = true;
                        break;
                    }
                }
                if (hybrid)
                    continue;

                if (spellInfo->Effects[0].Effect == effect)
                    potionCache[level][effect].push_back(itr.first);
            }
        }
    }

    for (uint32 level = 1; level <= maxLevel; level++)
    {
        uint32 effects[] = {SPELL_EFFECT_HEAL, SPELL_EFFECT_ENERGIZE};
        for (uint8 i = 0; i < 2; ++i)
        {
            uint32 effect = effects[i];
            uint32 size = potionCache[level][effect].size();
            counter += size;
        }
    }

    LOG_INFO("playerbots", "Cached {} potions", counter);
}

void RandomItemMgr::BuildFoodCache()
{
    uint32 maxLevel = sPlayerbotAIConfig.randomBotMaxLevel;
    if (maxLevel > sWorld->getIntConfig(CONFIG_MAX_PLAYER_LEVEL))
        maxLevel = sWorld->getIntConfig(CONFIG_MAX_PLAYER_LEVEL);

    LOG_INFO("server.loading", "Building food cache for {} levels", maxLevel);

    ItemTemplateContainer const* itemTemplates = sObjectMgr->GetItemTemplateStore();

    uint32 counter = 0;
    for (uint32 level = 1; level <= maxLevel + 1; level += 10)
    {
        uint32 categories[] = {11, 59};
        for (int i = 0; i < 2; ++i)
        {
            uint32 category = categories[i];

            for (auto const& itr : *itemTemplates)
            {
                ItemTemplate const* proto = &itr.second;
                if (!proto)
                    continue;

                if (proto->Class != ITEM_CLASS_CONSUMABLE ||
                    (proto->SubClass != ITEM_SUBCLASS_FOOD && proto->SubClass != ITEM_SUBCLASS_CONSUMABLE) ||
                    (proto->Spells[0].SpellCategory != category) || proto->Bonding != NO_BIND)
                    continue;

                if (proto->RequiredLevel && (proto->RequiredLevel > level || proto->RequiredLevel < level - 10))
                    continue;

                if (proto->RequiredSkill)
                    continue;

                if (proto->Area || proto->Map || proto->RequiredCityRank || proto->RequiredHonorRank)
                    continue;

                if (proto->Duration & 0x80000000)
                    continue;

                foodCache[level / 10][category].push_back(itr.first);
            }
        }
    }

    for (uint32 level = 1; level <= maxLevel + 1; level += 10)
    {
        uint32 categories[] = {11, 59};
        for (uint8 i = 0; i < 2; ++i)
        {
            uint32 category = categories[i];
            uint32 size = foodCache[level / 10][category].size();
            ++counter;
            LOG_DEBUG("server.loading", "Food cache for level={}, category={}: {} items", level, category, size);
        }
    }

    LOG_INFO("server.loading", "Cached {} types of food", counter);
}

uint32 RandomItemMgr::GetRandomPotion(uint32 level, uint32 effect)
{
    const std::vector<uint32> &potions = potionCache[level][effect];
    if (potions.empty())
        return 0;

    return potions[urand(0, potions.size() - 1)];
}

uint32 RandomItemMgr::GetFood(uint32 level, uint32 category)
{
    std::initializer_list<uint32> items;
    std::vector<uint32> food;
    if (category == 11)
    {
        if (level < 5)
            items = {787, 117, 4540, 2680};
        else if (level < 15)
            items = {2287, 4592, 4541, 21072};
        else if (level < 25)
            items = {3770, 16170, 4542, 20074};
        else if (level < 35)
            items = {4594, 3771, 1707, 4457};
        else if (level < 45)
            items = {4599, 4601, 21552, 17222 /*21030, 16168 */};
        else if (level < 55)
            items = {8950, 8952, 8957, 21023 /*21033, 21031 */};
        else if (level < 65)
            items = {29292, 27859, 30458, 27662};
        else if (level < 75)
            items = {29450, 29451, 29452};
        else
            items = {35947};
    }

    if (category == 59)
    {
        if (level < 5)
            items = {159, 117};
        else if (level < 15)
            items = {1179, 21072};
        else if (level < 25)
            items = {1205};
        else if (level < 35)
            items = {1708};
        else if (level < 45)
            items = {1645};
        else if (level < 55)
            items = {8766};
        else if (level < 65)
            items = {28399};
        else if (level < 75)
            items = {27860};
        else
            items = {33445};
    }

    food.insert(food.end(), items);
    if (food.empty())
        return 0;
    return food[urand(0, food.size() - 1)];
}

uint32 RandomItemMgr::GetRandomFood(uint32 level, uint32 category)
{
    std::vector<uint32> food = foodCache[(level - 1) / 10][category];
    if (food.empty())
        return 0;

    return food[urand(0, food.size() - 1)];
}

void RandomItemMgr::BuildTradeCache()
{
    uint32 maxLevel = sPlayerbotAIConfig.randomBotMaxLevel;
    if (maxLevel > sWorld->getIntConfig(CONFIG_MAX_PLAYER_LEVEL))
        maxLevel = sWorld->getIntConfig(CONFIG_MAX_PLAYER_LEVEL);

    LOG_INFO("server.loading", "Building trade cache for {} levels", maxLevel);

    ItemTemplateContainer const* itemTemplates = sObjectMgr->GetItemTemplateStore();

    uint32 counter = 0;
    for (uint32 level = 1; level <= maxLevel + 1; level += 10)
    {
        for (auto const& itr : *itemTemplates)
        {
            ItemTemplate const* proto = &itr.second;
            if (!proto)
                continue;

            if (proto->Class != ITEM_CLASS_TRADE_GOODS || proto->Bonding != NO_BIND)
                continue;

            if (proto->ItemLevel < level)
                continue;

            if (proto->RequiredLevel && (proto->RequiredLevel > level || proto->RequiredLevel < level - 10))
                continue;

            if (proto->RequiredSkill)
                continue;

            tradeCache[level / 10].push_back(itr.first);
        }
    }

    for (uint32 level = 1; level <= maxLevel + 1; level += 10)
    {
        uint32 size = tradeCache[level / 10].size();
        LOG_DEBUG("server.loading", "Trade cache for level={}: {} items", level, size);
        ++counter;
    }

    LOG_INFO("server.loading", "Cached {} trade items", counter);  // TEST
}

uint32 RandomItemMgr::GetRandomTrade(uint32 level)
{
    std::vector<uint32> trade = tradeCache[(level - 1) / 10];
    if (trade.empty())
        return 0;

    return trade[urand(0, trade.size() - 1)];
}

void RandomItemMgr::BuildRarityCache()
{
    if (PreparedQueryResult result =
            PlayerbotsDatabase.Query(PlayerbotsDatabase.GetPreparedStatement(PLAYERBOTS_SEL_RARITY_CACHE)))
    {
        LOG_INFO("playerbots", "Loading item rarity cache");

        uint32 count = 0;
        do
        {
            Field* fields = result->Fetch();
            uint32 itemId = fields[0].Get<uint32>();
            float rarity = fields[1].Get<float>();

            rarityCache[itemId] = rarity;
            ++count;

        } while (result->NextRow());

        LOG_INFO("playerbots", "Item rarity cache loaded from {} records", count);
    }
    else
    {
        ItemTemplateContainer const* itemTemplates = sObjectMgr->GetItemTemplateStore();
        LOG_INFO("playerbots", "Building item rarity cache from {} items", itemTemplates->size());

        for (auto const& itr : *itemTemplates)
        {
            ItemTemplate const* proto = &itr.second;
            if (!proto)
                continue;

            if (proto->Duration & 0x80000000)
                continue;

            if (proto->Quality == ITEM_QUALITY_POOR)
                continue;

            if (strstri(proto->Name1.c_str(), "qa") || strstri(proto->Name1.c_str(), "test") ||
                strstri(proto->Name1.c_str(), "deprecated"))
                continue;

            if (!proto->ItemLevel)
                continue;

            QueryResult results = WorldDatabase.Query(
                "SELECT MAX(q.chance) FROM ( "
                // "-- Creature "
                "SELECT  "
                "AVG ( "
                "   CASE  "
                "    WHEN lt.groupid = 0 THEN lt.ChanceOrQuestChance  "
                "    WHEN lt.ChanceOrQuestChance > 0 THEN lt.ChanceOrQuestChance "
                "    ELSE   "
                "    IFNULL(100 - (SELECT SUM(ChanceOrQuestChance) FROM creature_loot_template lt1 WHERE lt1.groupid = "
                "lt.groupid AND lt1.entry = lt.entry AND lt1.ChanceOrQuestChance > 0), 100) "
                "    / (SELECT COUNT(*) FROM creature_loot_template lt1 WHERE lt1.groupid = lt.groupid AND lt1.entry = "
                "lt.entry AND lt1.ChanceOrQuestChance = 0) "
                "    END "
                ") chance, 'creature' type "
                "FROM creature_loot_template lt "
                "JOIN creature_template ct ON ct.LootId = lt.entry "
                "JOIN creature c ON c.id1 = ct.entry "
                "WHERE lt.item = {} "
                "union all "
                // "-- Gameobject "
                "SELECT  "
                "AVG ( "
                "   CASE  "
                "    WHEN lt.groupid = 0 THEN lt.ChanceOrQuestChance  "
                "    WHEN lt.ChanceOrQuestChance > 0 THEN lt.ChanceOrQuestChance "
                "    ELSE   "
                "    IFNULL(100 - (SELECT SUM(ChanceOrQuestChance) FROM gameobject_loot_template lt1 WHERE lt1.groupid "
                "= lt.groupid AND lt1.entry = lt.entry AND lt1.ChanceOrQuestChance > 0), 100) "
                "    / (SELECT COUNT(*) FROM gameobject_loot_template lt1 WHERE lt1.groupid = lt.groupid AND lt1.entry "
                "= lt.entry AND lt1.ChanceOrQuestChance = 0) "
                "    END "
                ") chance, 'gameobject' type "
                "FROM gameobject_loot_template lt "
                "JOIN gameobject_template ct ON ct.data1 = lt.entry "
                "JOIN gameobject c ON c.id1 = ct.entry "
                "WHERE lt.item = {} "
                "union all "
                // "-- Disenchant "
                "SELECT  "
                "AVG ( "
                "   CASE  "
                "    WHEN lt.groupid = 0 THEN lt.ChanceOrQuestChance  "
                "    WHEN lt.ChanceOrQuestChance > 0 THEN lt.ChanceOrQuestChance "
                "    ELSE   "
                "    IFNULL(100 - (SELECT SUM(ChanceOrQuestChance) FROM disenchant_loot_template lt1 WHERE lt1.groupid "
                "= lt.groupid AND lt1.entry = lt.entry AND lt1.ChanceOrQuestChance > 0), 100) "
                "    / (SELECT COUNT(*) FROM disenchant_loot_template lt1 WHERE lt1.groupid = lt.groupid AND lt1.entry "
                "= lt.entry AND lt1.ChanceOrQuestChance = 0) "
                "    END "
                ") chance, 'disenchant' type "
                "FROM disenchant_loot_template lt "
                "JOIN item_template ct ON ct.DisenchantID = lt.entry "
                "WHERE lt.item = {} "
                "union all "
                // "-- Fishing "
                "SELECT  "
                "AVG ( "
                "   CASE  "
                "    WHEN lt.groupid = 0 THEN lt.ChanceOrQuestChance  "
                "    WHEN lt.ChanceOrQuestChance > 0 THEN lt.ChanceOrQuestChance "
                "    ELSE   "
                "    IFNULL(100 - (SELECT SUM(ChanceOrQuestChance) FROM fishing_loot_template lt1 WHERE lt1.groupid = "
                "lt.groupid AND lt1.entry = lt.entry AND lt1.ChanceOrQuestChance > 0), 100) "
                "    / (SELECT COUNT(*) FROM fishing_loot_template lt1 WHERE lt1.groupid = lt.groupid AND lt1.entry = "
                "lt.entry AND lt1.ChanceOrQuestChance = 0) "
                "    END "
                ") chance, 'fishing' type "
                "FROM fishing_loot_template lt "
                "WHERE lt.item = {} "
                "union all "
                // "-- Skinning "
                "SELECT  "
                "AVG ( "
                "   CASE  "
                "    WHEN lt.groupid = 0 THEN lt.ChanceOrQuestChance  "
                "    WHEN lt.ChanceOrQuestChance > 0 THEN lt.ChanceOrQuestChance  "
                "    ELSE   "
                "    IFNULL(100 - (SELECT SUM(ChanceOrQuestChance) FROM skinning_loot_template lt1 WHERE lt1.groupid = "
                "lt.groupid AND lt1.entry = lt.entry AND lt1.ChanceOrQuestChance > 0), 100) "
                "    * IFNULL((SELECT 1/COUNT(*) FROM skinning_loot_template lt1 WHERE lt1.groupid = lt.groupid AND "
                "lt1.entry = lt.entry AND lt1.ChanceOrQuestChance = 0), 1) "
                "    END "
                ") chance, 'skinning' type "
                "FROM skinning_loot_template lt "
                "JOIN creature_template ct ON ct.SkinningLootId = lt.entry "
                "JOIN creature c ON c.id1 = ct.entry "
                "WHERE lt.item = {}) q; ",
                itr.first, itr.first, itr.first, itr.first, itr.first);

            if (results)
            {
                Field* fields = results->Fetch();
                float rarity = fields[0].Get<float>();
                if (rarity > 0.01)
                {
                    rarityCache[itr.first] = rarity;

                    PlayerbotsDatabasePreparedStatement* stmt =
                        PlayerbotsDatabase.GetPreparedStatement(PLAYERBOTS_INS_RARITY_CACHE);
                    stmt->SetData(0, itr.first);
                    stmt->SetData(1, rarity);
                    PlayerbotsDatabase.Execute(stmt);
                }
            }
        }

        LOG_INFO("playerbots", "Item rarity cache built from {} items", itemTemplates->size());
    }
}

float RandomItemMgr::GetItemRarity(uint32 itemId) { return rarityCache[itemId]; }

inline bool IsCraftedBySpellInfo(ItemTemplate const* proto, SpellInfo const* spellInfo)
{
    for (uint32 x = 0; x < MAX_SPELL_REAGENTS; ++x)
    {
        if (spellInfo->Reagent[x] <= 0)
        {
            continue;
        }

        if (proto->ItemId == spellInfo->Reagent[x])
            return true;

    }

    for (uint8 i = 0; i < 3; ++i)
    {
        if (spellInfo->Effects[i].Effect == SPELL_EFFECT_CREATE_ITEM)
        {
            if (spellInfo->Effects[i].ItemType == proto->ItemId)
                return true;
        }
    }

    return false;
}

inline bool IsCraftedBySpell(ItemTemplate const* proto, uint32 spellId)
{
    SpellInfo const* spellInfo = sSpellMgr->GetSpellInfo(spellId);
    if (!spellInfo)
        return false;

    return IsCraftedBySpellInfo(proto, spellInfo);
}

inline bool IsCraftedBy(ItemTemplate const* proto, uint32 spellId)
{
    if (IsCraftedBySpell(proto, spellId))
        return true;

    SpellInfo const* spellInfo = sSpellMgr->GetSpellInfo(spellId);
    if (!spellInfo)
        return false;

    for (uint32 effect = 0; effect < 3; ++effect)
    {
        uint32 craftId = spellInfo->Effects[effect].TriggerSpell;
        SpellInfo const* craftSpellInfo = sSpellMgr->GetSpellInfo(craftId);
        if (!craftSpellInfo)
            continue;

        if (IsCraftedBySpellInfo(proto, craftSpellInfo))
            return true;
    }

    return false;
}

inline bool ContainsInternal(ItemTemplate const* proto, uint32 skillId)
{
    for (uint32 j = 0; j < sSkillLineAbilityStore.GetNumRows(); ++j)
    {
        SkillLineAbilityEntry const* skillLine = sSkillLineAbilityStore.LookupEntry(j);
        if (!skillLine || skillLine->ID != skillId)
            continue;

        if (IsCraftedBy(proto, skillLine->Spell))
            return true;
    }

    CreatureTemplateContainer const* creatures = sObjectMgr->GetCreatureTemplates();
    for (CreatureTemplateContainer::const_iterator itr = creatures->begin(); itr != creatures->end(); ++itr)
    {
        Trainer::Trainer* trainer = sObjectMgr->GetTrainer(itr->first);

        if (!trainer)
            continue;

        if (trainer->GetTrainerType() != Trainer::Type::Tradeskill)
            continue;

        for (auto& spell : trainer->GetSpells())
        {
            if (spell.ReqSkillLine != skillId)
                continue;

            if (IsCraftedBy(proto, spell.SpellId))
                return true;
        }
    }

    std::vector<ItemTemplate*> const* itemTemplates = sObjectMgr->GetItemTemplateStoreFast();
    for (ItemTemplate const* recipe : *itemTemplates)
    {
        if (!recipe)
            continue;

        if (recipe->Class == ITEM_CLASS_RECIPE &&
            ((recipe->SubClass == ITEM_SUBCLASS_LEATHERWORKING_PATTERN && skillId == SKILL_LEATHERWORKING) ||
             (recipe->SubClass == ITEM_SUBCLASS_TAILORING_PATTERN && skillId == SKILL_TAILORING) ||
             (recipe->SubClass == ITEM_SUBCLASS_ENGINEERING_SCHEMATIC && skillId == SKILL_ENGINEERING) ||
             (recipe->SubClass == ITEM_SUBCLASS_BLACKSMITHING && skillId == SKILL_BLACKSMITHING) ||
             (recipe->SubClass == ITEM_SUBCLASS_COOKING_RECIPE && skillId == SKILL_COOKING) ||
             (recipe->SubClass == ITEM_SUBCLASS_ALCHEMY_RECIPE && skillId == SKILL_ALCHEMY) ||
             (recipe->SubClass == ITEM_SUBCLASS_FIRST_AID_MANUAL && skillId == SKILL_FIRST_AID) ||
             (recipe->SubClass == ITEM_SUBCLASS_ENCHANTING_FORMULA && skillId == SKILL_ENCHANTING) ||
             (recipe->SubClass == ITEM_SUBCLASS_JEWELCRAFTING_RECIPE && skillId == SKILL_JEWELCRAFTING) ||
             (recipe->SubClass == ITEM_SUBCLASS_FISHING_MANUAL && skillId == SKILL_FISHING)))
        {
            for (uint32 i = 0; i < MAX_ITEM_PROTO_SPELLS; ++i)
            {
                if (IsCraftedBy(proto, recipe->Spells[i].SpellId))
                    return true;
            }
        }
    }

    return false;
}

bool RandomItemMgr::IsUsedBySkill(ItemTemplate const* proto, uint32 skillId)
{
    if (itemCache.find(proto->ItemId) != itemCache.end())
        return true;

    switch (proto->Class)
    {
        case ITEM_CLASS_TRADE_GOODS:
        case ITEM_CLASS_MISC:
        case ITEM_CLASS_REAGENT:
        case ITEM_CLASS_GEM:
            break;
        default:
            return false;
    }

    bool contains = ContainsInternal(proto, skillId);
    if (contains)
        itemCache.insert(proto->ItemId);

    return contains;
}
