// • ▌ ▄ ·.  ▄▄▄·  ▄▄ • ▪   ▄▄· ▄▄▄▄·  ▄▄▄·  ▐▄▄▄  ▄▄▄ .
// ·██ ▐███▪▐█ ▀█ ▐█ ▀ ▪██ ▐█ ▌▪▐█ ▀█▪▐█ ▀█ •█▌ ▐█▐▌·
// ▐█ ▌▐▌▐█·▄█▀▀█ ▄█ ▀█▄▐█·██ ▄▄▐█▀▀█▄▄█▀▀█ ▐█▐ ▐▌▐▀▀▀
// ██ ██▌▐█▌▐█ ▪▐▌▐█▄▪▐█▐█▌▐███▌██▄▪▐█▐█ ▪▐▌██▐ █▌▐█▄▄▌
// ▀▀  █▪▀▀▀ ▀  ▀ ·▀▀▀▀ ▀▀▀·▀▀▀ ·▀▀▀▀  ▀  ▀ ▀▀  █▪ ▀▀▀
//      Magicbane Emulator Project © 2013 - 2022
//                www.magicbane.com

package engine.gameManager;

import engine.InterestManagement.WorldGrid;
import engine.loot.WorkOrder;
import engine.math.Quaternion;
import engine.math.Vector3f;
import engine.math.Vector3fImmutable;
import engine.mbEnums;
import engine.net.Dispatch;
import engine.net.client.ClientConnection;
import engine.net.client.msg.ErrorPopupMsg;
import engine.net.client.msg.PetMsg;
import engine.objects.*;
import engine.powers.EffectsBase;
import engine.powers.RuneSkillAdjustEntry;
import engine.server.MBServerStatics;
import org.pmw.tinylog.Logger;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ThreadLocalRandom;
import java.util.concurrent.TimeUnit;

import static engine.math.FastMath.acos;

public enum NPCManager {

    NPC_MANAGER;
    public static HashMap<Integer, ArrayList<Integer>> _runeSetMap = new HashMap<>();

    public static void dismissNecroPet(Mob necroPet, boolean updateOwner) {

        necroPet.setCombatTarget(null);
        necroPet.hasLoot = false;

        if (necroPet.parentZone != null)
            necroPet.parentZone.zoneMobSet.remove(necroPet);

        try {
            necroPet.clearEffects();
        } catch (Exception e) {
            Logger.error(e.getMessage());
        }
        necroPet.playerAgroMap.clear();
        WorldGrid.RemoveWorldObject(necroPet);

        DbManager.removeFromCache(necroPet);


        PlayerCharacter petOwner = (PlayerCharacter) necroPet.guardCaptain;

        if (petOwner != null) {

            necroPet.guardCaptain = null;
            petOwner.setPet(null);

            if (updateOwner == false)
                return;

            PetMsg petMsg = new PetMsg(5, null);
            Dispatch dispatch = Dispatch.borrow(petOwner, petMsg);
            DispatchManager.dispatchMsgDispatch(dispatch, mbEnums.DispatchChannel.PRIMARY);
        }
    }

    public static void auditNecroPets(PlayerCharacter player) {
        int removeIndex = 0;
        while (player.necroPets.size() >= 10) {


            if (removeIndex == player.necroPets.size())
                break;

            Mob necroPet = player.necroPets.get(removeIndex);

            if (necroPet == null) {
                removeIndex++;
                continue;
            }
            dismissNecroPet(necroPet, true);
            player.necroPets.remove(necroPet);
            removeIndex++;


        }
    }

    public static void resetNecroPets(PlayerCharacter player) {

        for (Mob necroPet : player.necroPets)
            if (necroPet.isPet())
                necroPet.agentType = mbEnums.AIAgentType.MOBILE;
    }

    public static void spawnNecroPet(PlayerCharacter playerCharacter, Mob mob) {

        if (mob == null)
            return;

        if (mob.getMobBaseID() != 12021 && mob.getMobBaseID() != 12022)
            return;

        auditNecroPets(playerCharacter);
        resetNecroPets(playerCharacter);

        playerCharacter.necroPets.add(mob);
    }

    public static void dismissNecroPets(PlayerCharacter playerCharacter) {


        if (playerCharacter.necroPets.isEmpty())
            return;

        for (Mob necroPet : playerCharacter.necroPets) {

            try {
                dismissNecroPet(necroPet, true);
            } catch (Exception e) {
                Logger.error(e);
            }
        }
        playerCharacter.necroPets.clear();
    }

    public static void loadAllPirateNames() {

        DbManager.NPCQueries.LOAD_PIRATE_NAMES();
    }

    public static String getPirateName(int mobBaseID) {

        ArrayList<String> nameList = null;

        // If we cannot find name for this mobbase then
        // fallback to human male

        if (NPC._pirateNames.containsKey(mobBaseID))
            nameList = NPC._pirateNames.get(mobBaseID);
        else
            nameList = NPC._pirateNames.get(2111);

        if (nameList == null) {
            Logger.error("Null name list for 2111!");
        }

        return nameList.get(ThreadLocalRandom.current().nextInt(nameList.size()));

    }

    public static ArrayList<Building> getProtectedBuildings(NPC npc) {

        ArrayList<Building> protectedBuildings = new ArrayList<>();

        if (npc.building == null)
            return protectedBuildings;

        if (npc.building.getCity() == null)
            return protectedBuildings;

        for (Building b : npc.building.getCity().getParent().zoneBuildingSet) {

            if (b.getBlueprint() == null)
                continue;

            if (b.getProtectionState().equals(mbEnums.ProtectionState.CONTRACT))
                protectedBuildings.add(b);

            if (b.getProtectionState().equals(mbEnums.ProtectionState.PENDING))
                protectedBuildings.add(b);
        }

        return protectedBuildings;
    }

    public static int slotCharacterInBuilding(AbstractCharacter abstractCharacter) {

        int buildingSlot;

        if (abstractCharacter.building == null)
            return -1;

        // Get next available slot for this NPC and use it
        // to add the NPC to the building's hireling list
        // Account for R8's having slots reversed.

        if (abstractCharacter.building.getBlueprint() != null && abstractCharacter.building.getBlueprint().getBuildingGroup().equals(mbEnums.BuildingGroup.TOL) && abstractCharacter.building.getRank() == 8)
            buildingSlot = BuildingManager.getLastAvailableSlot(abstractCharacter.building);
        else
            buildingSlot = BuildingManager.getAvailableSlot(abstractCharacter.building);

        // Override slot for siege engines

        if (abstractCharacter.getObjectType().equals(mbEnums.GameObjectType.Mob) && ((Mob) abstractCharacter).behaviourType.equals(mbEnums.MobBehaviourType.SiegeEngine)) {
            Mob siegeMobile = (Mob) abstractCharacter;
            buildingSlot = siegeMobile.guardCaptain.minions.size() + 2;
        }

        if (buildingSlot == -1)
            Logger.error("No available slot for NPC: " + abstractCharacter.getObjectUUID());

        // Pets are regular mobiles not hirelings (Siege engines)
        if (abstractCharacter.contract != null)
            abstractCharacter.building.getHirelings().put(abstractCharacter, buildingSlot);

        // Override bind and location for  this npc derived
        // from BuildingManager slot location data.

        Vector3fImmutable slotLocation = BuildingManager.getSlotLocation(abstractCharacter.building, buildingSlot).getLocation();

        abstractCharacter.bindLoc = abstractCharacter.building.getLoc().add(slotLocation);

        // Rotate slot position by the building rotation

        abstractCharacter.bindLoc = Vector3fImmutable.rotateAroundPoint(abstractCharacter.building.getLoc(), abstractCharacter.bindLoc, abstractCharacter.building.getBounds().getQuaternion().angleY);

        abstractCharacter.loc = new Vector3fImmutable(abstractCharacter.bindLoc);

        // Rotate NPC rotation by the building's rotation

        Quaternion slotRotation = new Quaternion().fromAngles(0, acos(abstractCharacter.getRot().y) * 2, 0);
        slotRotation = slotRotation.mult(abstractCharacter.building.getBounds().getQuaternion());
        abstractCharacter.setRot(new Vector3f(0, slotRotation.y, 0));

        // Configure region and floor/level for this NPC

        abstractCharacter.region = BuildingManager.GetRegion(abstractCharacter.building, abstractCharacter.bindLoc.x, abstractCharacter.bindLoc.y, abstractCharacter.bindLoc.z);

        return buildingSlot;
    }

    public static void AssignPatrolPoints(Mob mob) {
        mob.patrolPoints = new ArrayList<>();

        for (int i = 0; i < 5; ++i) {
            float patrolRadius = mob.getSpawnRadius();

            if (patrolRadius > 256)
                patrolRadius = 256;

            if (patrolRadius < 60)
                patrolRadius = 60;

            Vector3fImmutable newPatrolPoint = Vector3fImmutable.getRandomPointInCircle(mob.getBindLoc(), patrolRadius);
            mob.patrolPoints.add(newPatrolPoint);

            if (i == 1) {
                mob.setLoc(newPatrolPoint);
                mob.endLoc = newPatrolPoint;
            }
        }
    }

    public static void applyGuardStanceModifiers(Mob guard) {
        float damageModifier = 1;
        float attackRatingModifier = 1;
        float defenseModifier = 1;
        float attackSpeedModifier = 1;
        float powerDamageModifier = 1;
        //handle stance modifiers for guard mob
        if (guard.agentType.equals(mbEnums.AIAgentType.GUARDWALLARCHER)) {
            //apply rogue bonuses
            attackRatingModifier += 0.5f;
            defenseModifier += 0.5f;
            damageModifier += 0.5f;
            attackSpeedModifier -= 0.36f;
        } else {
            Integer contractID;
            if (guard.agentType.equals(mbEnums.AIAgentType.GUARDMINION)) {
                contractID = guard.guardCaptain.contract.getContractID();
            } else {
                contractID = guard.contract.getContractID();
            }
            if (mbEnums.MinionType.ContractToMinionMap.get(contractID) != null && mbEnums.MinionType.ContractToMinionMap.get(contractID).isMage()) {
                //apply mage offensive Stance
                powerDamageModifier += 0.5f;
            } else {
                //apply fighter offensive stance
                damageModifier += 0.5f;
                attackSpeedModifier -= 0.36f;
            }
        }
        guard.minDamageHandOne *= damageModifier;
        guard.minDamageHandTwo *= damageModifier;
        guard.maxDamageHandOne *= damageModifier;
        guard.maxDamageHandTwo *= damageModifier;
        guard.atrHandOne *= attackRatingModifier;
        guard.atrHandTwo *= attackRatingModifier;
        guard.defenseRating *= defenseModifier;
        guard.speedHandOne *= attackSpeedModifier;
        guard.speedHandTwo *= attackSpeedModifier;

        //TODO figure out how to apply +50% powerdamage to mage guards
    }

    public static void setDamageAndSpeedForGuard(Mob guard) {

        float rankModifier = 1 + (guard.getRank() * 0.1f);
        int primaryStat = 0;
        if (guard.charItemManager.equipped.isEmpty()) {
            guard.minDamageHandOne = (int) ((guard.mobBase.getDamageMin()) * rankModifier);
            guard.maxDamageHandOne = (int) ((guard.mobBase.getDamageMax()) * rankModifier);
            guard.speedHandOne = 30.0f;
        } else {
            if (guard.charItemManager.equipped.containsKey(mbEnums.EquipSlotType.RHELD)) {
                //has main hand weapon
                Item weapon = guard.charItemManager.equipped.get(mbEnums.EquipSlotType.RHELD);

                if (weapon.template.item_primary_attr.equals(mbEnums.AttributeType.Strength))
                    primaryStat = guard.getStatStrCurrent();
                else
                    primaryStat = guard.getStatDexCurrent();

                guard.minDamageHandOne = (int) ((guard.mobBase.getDamageMin() + weapon.template.item_weapon_damage.values().iterator().next()[0]) * rankModifier) + primaryStat;
                guard.maxDamageHandOne = (int) ((guard.mobBase.getDamageMax() + weapon.template.item_weapon_damage.values().iterator().next()[1]) * rankModifier) + primaryStat;
                guard.speedHandOne = weapon.template.item_weapon_wepspeed;
                guard.rangeHandOne = weapon.template.item_weapon_max_range;
            } else if (guard.charItemManager.equipped.containsKey(mbEnums.EquipSlotType.LHELD) && !ItemManager.isShield(guard.charItemManager.equipped.get(mbEnums.EquipSlotType.LHELD).template)) {
                //has off hand weapon
                Item weapon = guard.charItemManager.equipped.get(mbEnums.EquipSlotType.LHELD);
                if (weapon.template.item_primary_attr.equals(mbEnums.AttributeType.Strength))
                    primaryStat = guard.getStatStrCurrent();
                else
                    primaryStat = guard.getStatDexCurrent();
                guard.minDamageHandOne = (int) ((guard.mobBase.getDamageMin() + weapon.template.item_weapon_damage.values().iterator().next()[0]) * rankModifier) + primaryStat;
                guard.maxDamageHandOne = (int) ((guard.mobBase.getDamageMax() + weapon.template.item_weapon_damage.values().iterator().next()[1]) * rankModifier) + primaryStat;
                guard.speedHandOne = weapon.template.item_weapon_wepspeed;
                guard.rangeHandOne = weapon.template.item_weapon_max_range;
            } else {
                primaryStat = guard.getStatStrCurrent();
                guard.minDamageHandOne = (int) ((guard.mobBase.getDamageMin()) * rankModifier) + primaryStat;
                guard.maxDamageHandOne = (int) ((guard.mobBase.getDamageMax()) * rankModifier) + primaryStat;
                guard.speedHandOne = 30.0f;
                guard.rangeHandOne = 3;
            }
        }
    }

    public static void setDefenseForGuard(Mob guard) {
        int dexterity = guard.getStatDexCurrent();
        if (dexterity < 1)
            dexterity = 1;
        int baseDef = guard.mobBase.getDefenseRating();
        int armorDefense = 0;
        for (Item equipped : guard.charItemManager.equipped.values())
            if (equipped.template.item_type.equals(mbEnums.ItemType.ARMOR) || ItemManager.isShield(equipped.template))
                armorDefense += equipped.template.item_defense_rating;
        guard.defenseRating = dexterity + baseDef + armorDefense;
    }

    public static void setAttackRatingForGuard(Mob guard) {
        int strength = guard.getStatStrCurrent();
        int baseAtr = guard.mobBase.getAttackRating();
        if (guard.charItemManager.equipped.get(mbEnums.EquipSlotType.RHELD) != null)
            guard.atrHandOne = baseAtr + (int) ((strength * 0.5f) + ((int) guard.charItemManager.equipped.get(mbEnums.EquipSlotType.RHELD).template.item_skill_required.values().toArray()[0] * 4) + ((int) guard.charItemManager.equipped.get(mbEnums.EquipSlotType.RHELD).template.item_skill_required.values().toArray()[0] * 3));
        else if (guard.charItemManager.equipped.get(mbEnums.EquipSlotType.LHELD) != null && !ItemManager.isShield(guard.charItemManager.equipped.get(mbEnums.EquipSlotType.LHELD).template))
            guard.atrHandTwo = baseAtr + (int) ((strength * 0.5f) + ((int) guard.charItemManager.equipped.get(mbEnums.EquipSlotType.LHELD).template.item_skill_required.values().toArray()[0] * 4) + ((int) guard.charItemManager.equipped.get(mbEnums.EquipSlotType.LHELD).template.item_skill_required.values().toArray()[0] * 3));
        else
            guard.atrHandOne = baseAtr;
    }

    public static void setMaxHealthForGuard(Mob guard) {
        //values derived fom reading memory address for health on client when selecting player guards
        switch (guard.getRank()) {
            default:
                guard.healthMax = 750; //rank 1
                break;
            case 2:
                guard.healthMax = 2082;
                break;
            case 3:
                guard.healthMax = 2740;
                break;
            case 4:
                guard.healthMax = 3414;
                break;
            case 5:
                guard.healthMax = 4080;
                break;
            case 6:
                guard.healthMax = 4746;
                break;
            case 7:
                guard.healthMax = 5412;
                break;
        }
        guard.setHealth(guard.healthMax);
    }

    public static void applyMobbaseEffects(Mob mob) {
        EffectsBase effectsBase;
        for (MobBaseEffects mbe : mob.mobBase.effectsList) {

            effectsBase = PowersManager.getEffectByToken(mbe.getToken());

            if (effectsBase == null) {
                Logger.info("Mob: " + mob.getObjectUUID() + "  EffectsBase Null for Token " + mbe.getToken());
                continue;
            }

            //check to upgrade effects if needed.
            if (mob.effects.containsKey(Integer.toString(effectsBase.getUUID()))) {

                if (mbe.getReqLvl() > (int) mob.level)
                    continue;

                Effect eff = mob.effects.get(Integer.toString(effectsBase.getUUID()));

                if (eff == null)
                    continue;

                //Current effect is a higher rank, dont apply.
                if (eff.getTrains() > mbe.getRank())
                    continue;

                //new effect is of a higher rank. remove old effect and apply new one.
                eff.cancelJob();
                mob.addEffectNoTimer(Integer.toString(effectsBase.getUUID()), effectsBase, mbe.getRank(), true);
            } else {

                if (mbe.getReqLvl() > (int) mob.level)
                    continue;

                mob.addEffectNoTimer(Integer.toString(effectsBase.getUUID()), effectsBase, mbe.getRank(), true);
            }

        }
    }

    public static void applyEquipmentResists(Mob mob) {

        if (mob.charItemManager.equipped.isEmpty())
            return;

        for (Item equipped : mob.charItemManager.equipped.values()) {
            if (equipped.template.item_type.equals(mbEnums.ItemType.ARMOR)) {
                mob.resists.setResist(mbEnums.DamageType.SLASHING, mob.resists.getResist(mbEnums.DamageType.SLASHING, 0) + equipped.template.combat_attack_resist.get("SLASHING"));
                mob.resists.setResist(mbEnums.DamageType.CRUSHING, mob.resists.getResist(mbEnums.DamageType.CRUSHING, 0) + equipped.template.combat_attack_resist.get("CRUSHING"));
                mob.resists.setResist(mbEnums.DamageType.PIERCING, mob.resists.getResist(mbEnums.DamageType.PIERCING, 0) + equipped.template.combat_attack_resist.get("PIERCING"));
            }
        }
    }

    public static void applyMobbaseSkill(Mob mob) {
        SkillsBase baseSkill = DbManager.SkillsBaseQueries.GET_BASE_BY_TOKEN(mob.mobBase.getMobBaseStats().getBaseSkill());
        if (baseSkill != null)
            mob.getSkills().put(baseSkill.getName(), new CharacterSkill(baseSkill, mob, mob.mobBase.getMobBaseStats().getBaseSkillAmount()));
    }

    public static void applyRuneSkills(Mob mob, int runeID) {
        //load mob skill adjustments from mobbase rune
        if (PowersManager._allRuneSkillAdjusts.containsKey(runeID))
            for (RuneSkillAdjustEntry entry : PowersManager._allRuneSkillAdjusts.get(runeID)) {
                if (SkillsBase.getFromCache(entry.skill_type) == null)
                    SkillsBase.putInCache(DbManager.SkillsBaseQueries.GET_BASE_BY_NAME(entry.skill_type));
                SkillsBase skillBase = SkillsBase.getFromCache(entry.skill_type);
                if (skillBase == null)
                    continue;
                if (entry.level <= mob.level)
                    if (mob.skills.containsKey(entry.name) == false)
                        mob.skills.put(entry.skill_type, new CharacterSkill(skillBase, mob, entry.rank));
                    else
                        mob.skills.put(entry.skill_type, new CharacterSkill(skillBase, mob, entry.rank + mob.skills.get(entry.skill_type).getNumTrains()));
            }
    }

    public static void applyRunesForNPC(NPC npc) {
        npc.runes = new ArrayList<>();
        RuneBase shopkeeperBase = RuneBase.getRuneBase(252620);
        CharacterRune shopkeeper = new CharacterRune(shopkeeperBase, npc.getObjectUUID());
        npc.runes.add(shopkeeper);
        if (NPCManager._runeSetMap.containsKey(npc.runeSetID)) {
            for (int runeID : _runeSetMap.get(npc.runeSetID)) {
                RuneBase rb = RuneBase.getRuneBase(runeID);
                if (rb != null) {
                    CharacterRune toApply = new CharacterRune(rb, npc.getObjectUUID());
                    npc.runes.add(toApply);
                }
            }
        }
    }

    public static Boolean NPCVaultBankRangeCheck(PlayerCharacter pc, ClientConnection origin, String bankorvault) {

        if (pc == null)
            return false;

        NPC npc = pc.getLastNPCDialog();

        if (npc == null)
            return false;

        // System.out.println(npc.getContract().getName());
        // last npc must be either a banker or vault keeper

        if (bankorvault.equals("vault")) {
            if (npc.getContract().getContractID() != 861)
                return false;
        } else
            // assuming banker

            if (!npc.getContract().getName().equals("Bursar"))
                return false;

        if (pc.getLoc().distanceSquared2D(npc.getLoc()) > MBServerStatics.NPC_TALK_RANGE * MBServerStatics.NPC_TALK_RANGE) {
            ErrorPopupMsg.sendErrorPopup(pc, 14);
            return false;
        } else
            return true;

    }

    public static ArrayList<Item> getAllCookingForVendor(NPC npc) {

        ArrayList<Item> itemList = new ArrayList<>();

        ConcurrentHashMap.KeySetView<WorkOrder, Boolean> vendorWorkOrders = ForgeManager.vendorWorkOrderLookup.get(npc);

        if (vendorWorkOrders == null)
            return itemList;

        for (WorkOrder workOrder : vendorWorkOrders)
            itemList.addAll(workOrder.cooking);

        return itemList;
    }

    public static long calcRollingDuration(NPC vendor, int templateID) {

        ItemTemplate template = ItemTemplate.templates.get(templateID);
        float rollingDuration;

        if (template == null)
            return 0;

        if (template.item_bane_rank > 0)
            return (long) (template.item_bane_rank * 60 * 60 * 3 * 1000 * Float.parseFloat(ConfigManager.MB_PRODUCTION_RATE.getValue()));

        if (vendor.building == null)
            return 600;

        rollingDuration = vendor.getBuilding().getRank() * -5L + 40;
        rollingDuration = TimeUnit.MINUTES.toMillis((long) rollingDuration);
        rollingDuration *= Float.parseFloat(ConfigManager.MB_PRODUCTION_RATE.getValue());

        return (long) rollingDuration;
    }
}
