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


package engine.objects;

import engine.InterestManagement.WorldGrid;
import engine.gameManager.*;
import engine.job.JobContainer;
import engine.job.JobScheduler;
import engine.jobs.UpgradeNPCJob;
import engine.math.Bounds;
import engine.math.Vector3f;
import engine.math.Vector3fImmutable;
import engine.mbEnums;
import engine.mbEnums.*;
import engine.net.ByteBufferWriter;
import engine.net.Dispatch;
import engine.net.client.msg.PetMsg;
import engine.net.client.msg.PlaceAssetMsg;
import engine.server.MBServerStatics;
import org.joda.time.DateTime;
import org.pmw.tinylog.Logger;

import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.locks.ReentrantReadWriteLock;

import static engine.net.client.msg.ErrorPopupMsg.sendErrorPopup;
import static engine.objects.MobBase.loadEquipmentSet;
import static engine.util.StringUtils.wordCount;

public class NPC extends AbstractCharacter {

    public static int SVR_CLOSE_WINDOW = 4;
    public static HashMap<Integer, ArrayList<String>> _pirateNames = new HashMap<>();
    // Used for thread safety
    public final ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
    public ReentrantReadWriteLock minionLock = new ReentrantReadWriteLock();
    public int runeSetID = 0;
    public int extraRune2 = 0;
    public int loadID;
    protected MobBase mobBase;
    protected String name;
    protected int dbID;
    protected int currentID;
    //used by static npcs
    protected Zone parentZone;
    protected float statLat;
    protected float statLon;
    protected float statAlt;
    public float sellPercent; //also train percent
    public float buyPercent;
    protected int vendorID;
    protected ArrayList<Integer> modTypeTable;
    protected ArrayList<Integer> modSuffixTable;
    protected ArrayList<Integer> itemModTable;
    protected int symbol;
    // Variables NOT to be stored in db
    protected boolean isStatic = false;
    private DateTime upgradeDateTime = null;
    private HashSet<Integer> canRoll = null;
    public int parentZoneUUID;
    public int equipmentSetID = 0;
    private int repairCost = 5;

    // New NPC constructor.  Fill in the blanks and then call
    // PERSIST.

    public NPC() {

        super();
        this.dbID = MBServerStatics.NO_DB_ROW_ASSIGNED_YET;
        this.currentID = MBServerStatics.NO_DB_ROW_ASSIGNED_YET;
    }

    /**
     * ResultSet Constructor
     */
    public NPC(ResultSet rs) throws SQLException {

        super(rs);

        java.util.Date sqlDateTime;

        try {
            this.dbID = rs.getInt(1);
            this.currentID = this.dbID;
            this.setObjectTypeMask(MBServerStatics.MASK_NPC);
            this.contractUUID = rs.getInt("npc_contractID");
            this.parentZoneUUID = rs.getInt("parent");
            this.gridObjectType = GridObjectType.STATIC;
            this.equipmentSetID = rs.getInt("equipmentSet");
            this.runeSetID = rs.getInt("runeSet");

            this.loadID = rs.getInt("npc_raceID");

            this.level = rs.getByte("npc_level");

            buildingUUID = rs.getInt("npc_buildingID");

            // Most objects from the cache have a default buy
            // percentage of 100% which was a dupe source due
            // to the way MB calculated item values.

            // this.buyPercent = rs.getFloat("npc_buyPercent");

            this.buyPercent = .33f;
            this.sellPercent = 1;

            this.setRot(new Vector3f(0, rs.getFloat("npc_rotation"), 0));

            this.statLat = rs.getFloat("npc_spawnX");
            this.statAlt = rs.getFloat("npc_spawnY");
            this.statLon = rs.getFloat("npc_spawnZ");

            this.guildUUID = rs.getInt("npc_guildID");

            // Set upgrade date JodaTime DateTime object
            // if one exists in the database.

            sqlDateTime = rs.getTimestamp("upgradeDate");

            if (sqlDateTime != null)
                upgradeDateTime = new DateTime(sqlDateTime);
            else
                upgradeDateTime = null;

            // Submit upgrade job if NPC is currently set to rank.

            if (this.upgradeDateTime != null)
                submitUpgradeJob();

            this.name = rs.getString("npc_name");

        } catch (Exception e) {
            Logger.error("NPC: " + this.dbID + " :" + e);
            e.printStackTrace();
        }

    }

    public static boolean ISWallArcher(Contract contract) {

        if (contract == null)
            return false;

        //838, 950, 1051, 1181, 1251, 1351, 1451, 1501, 1526, 1551, 980101,

        if (contract.allowedBuildings.contains(BuildingGroup.WALLCORNER))
            return true;
        return contract.allowedBuildings.contains(BuildingGroup.WALLSTRAIGHTTOWER);
    }

    //This method restarts an upgrade timer when a building is loaded from the database.
    // Submit upgrade job for this building based upon it's current upgradeDateTime

    public static NPC getFromCache(int id) {
        return (NPC) DbManager.getFromCache(GameObjectType.NPC, id);
    }

    public static boolean UpdateName(NPC npc, String value) {

        if (!DbManager.NPCQueries.UPDATE_NAME(npc, value))
            return false;

        npc.name = value;
        return true;

    }

    public static void serializeNpcForClientMsgOtherPlayer(NPC npc, ByteBufferWriter writer, boolean hideAsciiLastName) {
        serializeForClientMsgOtherPlayer(npc, writer);
    }

    public static void serializeForClientMsgOtherPlayer(NPC npc, ByteBufferWriter writer) {

        writer.putInt(0);
        writer.putInt(0);

        //num Runes
        int cnt = 3;
        boolean isVamp = false, isHealer = false, isArcher = false, isTrainer = false;
        int contractID = 0, classID = 0;
        int extraRune = 0;

        if (npc.contract != null) {
            contractID = npc.contract.getContractID();
            classID = npc.contract.getClassID();
            extraRune = npc.contract.getExtraRune();

            if (extraRune == contractID)
                extraRune = 0;

        }

        if ((contractID > 252642 && contractID < 252647) || contractID == 252652) {
            isVamp = true;
            cnt++;
        }

        if (contractID == 252582 || contractID == 252579 || contractID == 252581
                || contractID == 252584 || contractID == 252597 || contractID == 252598
                || contractID == 252628 || extraRune == 252582 || extraRune == 252579
                || extraRune == 252581 || extraRune == 252584 || extraRune == 252597
                || extraRune == 252598 || extraRune == 252628) {
            isHealer = true;
            cnt++;
        }

        if (contractID == 252570) {
            isArcher = true;
            cnt++;
        }

        if (classID != 0)
            cnt++;

        if (extraRune != 0 && extraRune != contractID)
            cnt++;

        writer.putInt(cnt);

        //Race
        writer.putInt(1);
        writer.putInt(0);

        if (npc.mobBase != null)
            writer.putInt(npc.mobBase.getLoadID());
        else
            writer.putInt(2011);

        writer.putInt(GameObjectType.NPCRaceRune.ordinal());
        writer.putInt(npc.currentID);

        //Class/Trainer/Whatever
        writer.putInt(5);
        writer.putInt(0);

        if (npc.contract != null)
            writer.putInt(contractID);
        else
            writer.putInt(2500);

        writer.putInt(GameObjectType.NPCClassRune.ordinal());
        writer.putInt(npc.currentID);

        //vampire trainer
        cnt = 0;

        if (extraRune != 0)
            cnt = serializeExtraRune(npc, extraRune, cnt, writer);
        if (isVamp)
            cnt = serializeExtraRune(npc, 252647, cnt, writer);

        //Healer trainer
        if (isHealer)
            cnt = serializeExtraRune(npc, 252592, cnt, writer);

        if (classID != 0) {
            writer.putInt(4);
            writer.putInt(0);
            writer.putInt(classID);
            writer.putInt(GameObjectType.NPCExtraRune.ordinal());
            writer.putInt(npc.currentID);
        }

        //Scout trainer
        if (isArcher)
            cnt = serializeExtraRune(npc, 252654, cnt, writer);

        //Shopkeeper
        writer.putInt(5);
        writer.putInt(0);
        writer.putInt(0x3DACC);
        writer.putInt(GameObjectType.NPCShopkeeperRune.ordinal());
        writer.putInt(npc.currentID);

        //Send Stats
        writer.putInt(5);
        writer.putInt(0x8AC3C0E6); //Str
        writer.putInt(0);
        writer.putInt(0xACB82E33); //Dex
        writer.putInt(0);
        writer.putInt(0xB15DC77E); //Con
        writer.putInt(0);
        writer.putInt(0xE07B3336); //Int
        writer.putInt(0);
        writer.putInt(0xFF665EC3); //Spi
        writer.putInt(0);

        writer.putString(npc.name);
        writer.putString("");

        writer.putInt(0);
        writer.putInt(0);
        writer.putInt(0);
        writer.putInt(0);

        writer.put((byte) 0);
        writer.putInt(npc.getObjectType().ordinal());
        writer.putInt(npc.currentID);

        writer.putFloat(1.0f);
        writer.putFloat(1.0f);
        writer.putFloat(1.0f);

        if (npc.region != null)
            writer.putVector3f(ZoneManager.convertWorldToLocal(npc.building, npc.getLoc()));
        else
            writer.putVector3f(npc.getLoc());

        //Rotation
        float radians = (float) Math.acos(npc.getRot().y) * 2;

        if (npc.building != null)
            if (npc.building.getBounds() != null && npc.building.getBounds().getQuaternion() != null)
                radians += (npc.building.getBounds().getQuaternion()).angleY;

        writer.putFloat(radians);

        //Running Speed
        writer.putInt(0);

        // get a copy of the equipped items.

        if (npc.charItemManager.equipped.isEmpty() == false) {
            writer.putInt(npc.charItemManager.equipped.size());

            for (Item me : npc.charItemManager.equipped.values())
                Item._serializeForClientMsg(me, writer);
        } else
            writer.putInt(0);

        writer.putInt((npc.level / 10));
        writer.putInt(npc.level);
        writer.putInt(npc.getIsSittingAsInt()); //Standing
        writer.putInt(npc.getIsWalkingAsInt()); //Walking
        writer.putInt(npc.getIsCombatAsInt()); //Combat
        writer.putInt(2); //Unknown
        writer.putInt(1); //Unknown - Headlights?
        writer.putInt(0);

        if (npc.building != null && npc.region != null) {
            writer.putInt(npc.building.getObjectType().ordinal());
            writer.putInt(npc.building.getObjectUUID());
        } else {
            writer.putInt(0); //<-Building Object Type
            writer.putInt(0); //<-Building Object ID
        }
        writer.put((byte) 0);
        writer.put((byte) 0);
        writer.put((byte) 0);

        //npc dialog menus from contracts

        if (npc.contract != null) {
            ArrayList<Integer> npcMenuOptions = npc.contract.getNPCMenuOptions();
            writer.putInt(npcMenuOptions.size());
            for (Integer val : npcMenuOptions) {
                writer.putInt(val);
            }

        } else
            writer.putInt(0);

        writer.put((byte) 1);

        if (npc.building != null) {
            writer.putInt(GameObjectType.StrongBox.ordinal());
            writer.putInt(npc.currentID);
            writer.putInt(GameObjectType.StrongBox.ordinal());
            writer.putInt(npc.building.getObjectUUID());
        } else {
            writer.putLong(0);
            writer.putLong(0);
        }

        if (npc.contract != null)
            writer.putInt(npc.contract.getIconID());
        else
            writer.putInt(0); //npc icon ID

        writer.putInt(0);
        writer.putShort((short) 0);

        if (npc.contract != null && npc.contract.isTrainer()) {
            writer.putInt(classID);
        } else {
            writer.putInt(0);
        }

        if (npc.contract != null && npc.contract.isTrainer())
            writer.putInt(classID);
        else
            writer.putInt(0);

        writer.putInt(0);
        writer.putInt(0);

        writer.putFloat(4);
        writer.putInt(0);
        writer.putInt(0);
        writer.putInt(0);
        writer.put((byte) 0);

        //Pull guild info from building if linked to one

        Guild.serializeForClientMsg(npc.guild, writer, null, true);

        writer.putInt(1);
        writer.putInt(0x8A2E);
        writer.putInt(0);
        writer.putInt(0);

        //TODO Guard
        writer.put((byte) 0); //Is guard..

        writer.putFloat(1500f); //npc.healthMax
        writer.putFloat(1500f); //npc.health

        //TODO Peace Zone
        writer.put((byte) 1); //0=show tags, 1=don't
        writer.putInt(0);
        writer.put((byte) 0);
    }

    private static int serializeExtraRune(NPC npc, int runeID, int cnt, ByteBufferWriter writer) {

        writer.putInt(5);
        writer.putInt(0);
        writer.putInt(runeID);

        if (cnt == 0)
            writer.putInt(GameObjectType.NPCClassRuneTwo.ordinal());
        else
            writer.putInt(GameObjectType.NPCClassRuneThree.ordinal());

        writer.putInt(npc.currentID);
        return cnt + 1;
    }

    public static NPC createNPC(String name, int contractID, Vector3fImmutable spawn, Guild guild, Zone parent, short level, Building building) {

        NPC newNPC = new NPC();

        newNPC.parentZoneUUID = parent.getObjectUUID();

        newNPC.name = name;
        newNPC.contractUUID = contractID;

        if (building == null)
            newNPC.bindLoc = ZoneManager.worldToLocal(spawn, parent);
        else
            newNPC.bindLoc = Vector3fImmutable.ZERO;

        if (guild == null)
            newNPC.guildUUID = 0;
        else
            newNPC.guildUUID = guild.getObjectUUID();

        if (building == null)
            newNPC.buildingUUID = 0;
        else
            newNPC.buildingUUID = building.getObjectUUID();

        newNPC.level = level;

        newNPC.buyPercent = .33f;
        newNPC.sellPercent = 1;

        NPC npc;

        try {
            npc = DbManager.NPCQueries.PERSIST(newNPC);
            npc.setObjectTypeMask(MBServerStatics.MASK_NPC);
        } catch (Exception e) {
            Logger.error(e);
            npc = null;
        }

        return npc;
    }

    public static NPC getNPC(int id) {

        if (id == 0)
            return null;

        NPC npc = (NPC) DbManager.getFromCache(GameObjectType.NPC, id);

        if (npc != null)
            return npc;

        return DbManager.NPCQueries.GET_NPC(id);
    }

    /*
     * Getters
     */

    public static boolean ISGuardCaptain(int contractID) {
        return MinionType.ContractToMinionMap.containsKey(contractID);
    }

    public static NPCProfits GetNPCProfits(NPC npc) {
        return NPCProfits.ProfitCache.get(npc.currentID);
    }

    public final void submitUpgradeJob() {

        JobContainer jc;

        if (this.getUpgradeDateTime() == null) {
            Logger.error("Attempt to submit upgrade job for non-ranking NPC");
            return;
        }

        // Submit upgrade job for future date or current instant

        if (this.getUpgradeDateTime().isAfter(DateTime.now()))
            jc = JobScheduler.getInstance().scheduleJob(new UpgradeNPCJob(this),
                    this.getUpgradeDateTime().getMillis());
        else
            JobScheduler.getInstance().scheduleJob(new UpgradeNPCJob(this), 0);

    }

    public void setRank(int newRank) {

        DbManager.NPCQueries.SET_PROPERTY(this, "npc_level", newRank);
        this.level = (short) newRank;
    }

    public int getDBID() {
        return this.dbID;
    }

    @Override
    public int getObjectUUID() {
        return currentID;
    }

    public MobBase getMobBase() {
        return this.mobBase;
    }

    public Contract getContract() {
        return this.contract;
    }

    public int getContractID() {

        if (this.contract != null)
            return this.contract.getObjectUUID();

        return 0;
    }

    public boolean isStatic() {
        return this.isStatic;
    }

    public Building getBuilding() {
        return this.building;
    }

    public void setBuilding(Building value) {
        this.building = value;
    }

    @Override
    public String getFirstName() {
        return this.name;
    }

    @Override
    public String getName() {
        return this.name;
    }

    public void setName(String value) {
        this.name = value;
    }

    @Override
    public String getLastName() {
        return "";
    }

    @Override
    public Vector3fImmutable getBindLoc() {
        return this.bindLoc;
    }

    @Override
    public int getGuildUUID() {

        if (this.guild == null)
            return 0;

        return this.guild.getObjectUUID();
    }

    public void removeMinions() {


        for (Integer minionUUID : this.minions) {

            Mob minionMob = Mob.getMob(minionUUID);

            try {
                minionMob.clearEffects();
            } catch (Exception e) {
                Logger.error(e.getMessage());
            }

            if (minionMob.parentZone != null) {
                minionMob.parentZone.zoneMobSet.remove(minionMob);
            }

            WorldGrid.RemoveWorldObject(minionMob);
            DbManager.removeFromCache(minionMob);


            PlayerCharacter petOwner = (PlayerCharacter) minionMob.guardCaptain;

            if (petOwner != null) {

                petOwner.setPet(null);

                minionMob.guardCaptain = null;

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

            }
        }

    }

    @Override
    public float getSpeed() {

        if (this.isWalk())
            return MBServerStatics.WALKSPEED;
        else
            return MBServerStatics.RUNSPEED;
    }

    @Override
    public float getPassiveChance(String type, int AttackerLevel, boolean fromCombat) {
        //TODO add this later for dodge
        return 0f;
    }

    /**
     * @ Kill this Character
     */
    @Override
    public void killCharacter(AbstractCharacter attacker) {
        //TODO Handle Death
        killCleanup();

        //TODO Send death message if needed
    }

    @Override
    public void killCharacter(String reason) {
        //TODO Handle Death
        killCleanup();

        //Orphan inventory so it can be looted
        //if (!this.inSafeZone)
        if (this.charItemManager != null)
            this.charItemManager.orphanInventory();

        //TODO Send death message if needed
        //Question? How would a mob die to water?
    }

    private void killCleanup() {
        //TODO handle cleanup from death here

        //set so character won't load
        this.load = false;

        //Create Corpse and add to world
        //Corpse.makeCorpse(this);
        //TODO damage equipped items
        //TODO cleanup any timers
    }

    public Zone getParentZone() {
        return this.parentZone;
    }

    public int getParentZoneUUID() {

        if (this.parentZone != null)
            return this.parentZone.getObjectUUID();

        return 0;
    }

    @Override
    public Vector3fImmutable getLoc() {

        return super.getLoc();
    }

    public float getSellPercent() {
        return this.sellPercent;
    }

    public float getBuyPercent() {
        return this.buyPercent;
    }

    public float getBuyPercent(PlayerCharacter player) {
        if (NPC.GetNPCProfits(this) == null || this.guild == null)
            return this.buyPercent;
        NPCProfits profits = NPC.GetNPCProfits(this);
        if (player.getGuild().equals(this.guild))
            return profits.buyGuild;
        if (player.getGuild().getNation().equals(this.guild.getNation()))
            return profits.buyNation;

        return profits.buyNormal;
    }

    public float getSellPercent(PlayerCharacter player) {
        if (NPC.GetNPCProfits(this) == null || this.guild == null)
            return 1 + this.sellPercent;
        NPCProfits profits = NPC.GetNPCProfits(this);
        if (player.getGuild().equals(this.guild))
            return 1 + profits.sellGuild;
        if (player.getGuild().getNation().equals(this.guild.getNation()))
            return 1 + profits.sellNation;

        return 1 + profits.sellNormal;
    }

    @Override
    public boolean canBeLooted() {
        return !this.isAlive();
    }

    public ArrayList<Integer> getModTypeTable() {
        return this.modTypeTable;
    }

    @Override
    public void updateDatabase() {
        DbManager.NPCQueries.updateDatabase(this);
    }

    public int getSymbol() {
        return symbol;
    }

    public ArrayList<Integer> getModSuffixTable() {
        return modSuffixTable;
    }

    public ArrayList<Integer> getItemModTable() {
        return itemModTable;
    }

    public boolean isRanking() {

        return this.upgradeDateTime != null;
    }

    @Override
    public void runAfterLoad() {

        if (ConfigManager.serverType.equals(ServerType.LOGINSERVER))
            return;

        this.contract = DbManager.ContractQueries.GET_CONTRACT(this.contractUUID);

        if (this.equipmentSetID == 0 && this.contract != null)
            this.equipmentSetID = this.contract.equipmentSet;

        // Default to contract load ID

        if (loadID == 0) {

            if (this.contract != null)
                loadID = this.contract.getMobbaseID();
            else {
                Logger.error("Invalid contract for NPC: " + this.getObjectUUID());
                loadID = 2100; // Default human male
            }
        }

        this.mobBase = MobBase.getMobBase(this.loadID);

        this.building = BuildingManager.getBuilding(this.buildingUUID);

        if (this.building != null)
            this.guild = this.building.getGuild();
        else
            this.guild = Guild.getGuild(this.guildUUID);

        if (this.guild == null)
            this.guild = Guild.getErrantGuild();

        if (this.contract == null)
            return; // Early exit for npc guild owners

        // Name override for this npc

        if (wordCount(this.name) < 2 && this.contract != null)
            this.name += " the " + this.contract.getName();

        // Configure parent zone adding this NPC to the
        // zone collection

        this.parentZone = ZoneManager.getZoneByUUID(this.parentZoneUUID);
        this.parentZone.zoneNPCSet.remove(this);
        this.parentZone.zoneNPCSet.add(this);

        // Setup location for this NPC

        this.bindLoc = new Vector3fImmutable(this.statLat, this.statAlt, this.statLon);
        this.bindLoc = this.parentZone.getLoc().add(this.bindLoc);
        this.setLoc(bindLoc);

        // Handle NPCs within buildings

        if (this.building != null)
            NPCManager.slotCharacterInBuilding(this);

        // If NPC is a vendor then configure ForgeManager support

        if (this.contract.isTrainer() == false)
            ForgeManager.vendorWorkOrderLookup.computeIfAbsent(this, k -> ConcurrentHashMap.newKeySet());

        this.symbol = this.contract.getIconID();
        this.modTypeTable = this.contract.getNPCModTypeTable();
        this.modSuffixTable = this.contract.getNpcModSuffixTable();
        this.itemModTable = this.contract.getItemModTable();
        int VID = this.contract.getVendorID();

        if (VID != 0)
            this.vendorID = VID;
        else
            this.vendorID = 1; //no vendor items

        this.healthMax = this.mobBase.getHealthMax();
        this.manaMax = 0;
        this.staminaMax = 0;
        this.setHealth(this.healthMax);
        this.mana.set(this.manaMax);
        this.stamina.set(this.staminaMax);

        if (this.parentZone.guild_zone)
            if (NPC.GetNPCProfits(this) == null)
                NPCProfits.CreateProfits(this);

        //TODO set these correctly later
        this.rangeHandOne = 8;
        this.rangeHandTwo = -1;
        this.minDamageHandOne = 1;
        this.maxDamageHandOne = 4;
        this.minDamageHandTwo = 1;
        this.maxDamageHandTwo = 4;
        this.atrHandOne = 300;
        this.defenseRating = 200;
        this.isActive = true;

        this.charItemManager.load();

        if (equipmentSetID != 0 && LootManager._bootySetMap.get(equipmentSetID) == null)
            Logger.error("Invalid equipSet: " + equipmentSetID + " contract: " + this.contractUUID + " npc: " + this.getObjectUUID());

        this.charItemManager.equipped = loadEquipmentSet(this.equipmentSetID);

        // Create NPC bounds object
        Bounds npcBounds = Bounds.borrow();
        npcBounds.setBounds(this.getLoc());

        //apply NPC rune effects
        NPCManager.applyRunesForNPC(this);
        this.resists.setImmuneToAll(true);

    }

    public void removeFromZone() {
        this.parentZone.zoneNPCSet.remove(this);
    }

    @Override
    protected ConcurrentHashMap<Integer, CharacterPower> initializePowers() {
        return new ConcurrentHashMap<>(MBServerStatics.CHM_INIT_CAP, MBServerStatics.CHM_LOAD, MBServerStatics.CHM_THREAD_LOW);
    }

    public DateTime getUpgradeDateTime() {
        lock.readLock().lock();
        try {
            return upgradeDateTime;
        } finally {
            lock.readLock().unlock();
        }
    }

    public void setUpgradeDateTime(DateTime upgradeDateTime) {

        if (!DbManager.NPCQueries.updateUpgradeTime(this, upgradeDateTime)) {
            Logger.error("Failed to set upgradeTime for building " + currentID);
            return;
        }

        this.upgradeDateTime = upgradeDateTime;
    }

    @Override
    public Guild getGuild() {
        if (this.building != null)
            return building.getGuild();
        return this.guild;
    }

    public HashSet<Integer> getCanRoll() {

        if (this.canRoll == null)
            this.canRoll = DbManager.ItemQueries.GET_VENDOR_CAN_ROLL_LIST(this.vendorID);

        HashSet<Integer> fullItemList = this.canRoll;
        HashSet<Integer> filteredItemList = new HashSet<>();
        short maxSkill = 25;

        switch (this.getRank()) {
            case 1:
                maxSkill = 25;
                break;
            case 2:
                maxSkill = 50;
                break;
            case 3:
            case 4:
                maxSkill = 75;
                break;
            case 5:
            case 6:
                maxSkill = 100;
                break;
            case 7:
                maxSkill = 110;
                break;
        }
        ItemTemplate itemTemplate;
        int skillRequired = 0;
        for (Integer itemID : fullItemList) {
            itemTemplate = ItemTemplate.templates.get(itemID);

            if (itemTemplate.item_skill_required.values().iterator().hasNext())
                skillRequired = itemTemplate.item_skill_required.values().iterator().next();

            boolean exclude = skillRequired == 0 && ItemTemplate.templates.get(itemID).item_type.equals(ItemType.WEAPON);
            if (skillRequired <= maxSkill && !exclude)
                filteredItemList.add(itemID);
        }
        if (this.contract.getVendorID() == 102) {

            for (int i = 0; i < this.getRank(); i++) {
                int subID = i + 1;
                filteredItemList.add(910010 + subID);
            }

            if (this.getRank() == 7)
                filteredItemList.add(910018);
        }

        return filteredItemList;
    }

    public boolean remove() {

        Building building;

        // Remove npc from it's building

        building = this.building;

        if (building != null) {
            building.getHirelings().remove(this);
            this.removeMinions();
        }

        // Delete npc from database

        if (DbManager.NPCQueries.DELETE_NPC(this) == 0) {
            return false;
        }

        // Remove npc from the simulation

        this.removeFromCache();
        WorldGrid.RemoveWorldObject(this);
        WorldGrid.removeObject(this);
        return true;
    }

    public int getUpgradeCost() {

        int upgradeCost;

        upgradeCost = Integer.MAX_VALUE;

        if (this.getRank() < 7)
            return (this.getRank() * 100650) + 21450;

        return upgradeCost;
    }

    public int getUpgradeTime() {

        int upgradeTime; // expressed in hours

        upgradeTime = Integer.MAX_VALUE;

        if (this.getRank() < 7)
            return (this.getRank() * 8);

        return 0;
    }


    public int getEquipmentSetID() {
        return equipmentSetID;
    }

    public String getNameOverride() {
        return name;
    }

    public int getRepairCost() {
        return repairCost;
    }

    public void setRepairCost(int repairCost) {
        this.repairCost = repairCost;
    }

    public void processUpgradeNPC(PlayerCharacter player) {

        int rankCost;
        Building building;
        DateTime dateToUpgrade;


        this.lock.writeLock().lock();
        try {


            building = this.getBuilding();

            // Cannot upgrade an npc not within a building

            if (building == null)
                return;

            // Cannot upgrade an npc at max rank

            if (this.getRank() == 7)
                return;

            // Cannot upgrade an npc who is currently ranking

            if (this.isRanking())
                return;

            rankCost = this.getUpgradeCost();

            // SEND NOT ENOUGH GOLD ERROR

            if (!building.hasFunds(rankCost)) {
                sendErrorPopup(player, 127);
                return;
            }

            if (rankCost > building.getStrongboxValue()) {
                sendErrorPopup(player, 127);
                return;
            }

            try {

                if (!building.transferGold(-rankCost, false))
                    return;

                dateToUpgrade = DateTime.now().plusHours(this.getUpgradeTime());

                this.setUpgradeDateTime(dateToUpgrade);

                // Schedule upgrade job

                this.submitUpgradeJob();

            } catch (Exception e) {
                PlaceAssetMsg.sendPlaceAssetError(player.getClientConnection(), 1, "A Serious error has occurred. Please post details for to ensure transaction integrity");
            }
        } catch (Exception e) {
            Logger.error(e);
        } finally {
            this.lock.writeLock().unlock();
        }
    }

}
