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


package engine.objects;

import engine.InterestManagement.RealmMap;
import engine.InterestManagement.WorldGrid;
import engine.db.archive.CityRecord;
import engine.db.archive.DataWarehouse;
import engine.gameManager.*;
import engine.math.Bounds;
import engine.math.FastMath;
import engine.math.Vector2f;
import engine.math.Vector3fImmutable;
import engine.mbEnums;
import engine.mbEnums.*;
import engine.net.ByteBufferWriter;
import engine.net.Dispatch;
import engine.net.client.msg.ErrorPopupMsg;
import engine.net.client.msg.TaxResourcesMsg;
import engine.net.client.msg.ViewResourcesMsg;
import engine.powers.EffectsBase;
import engine.server.MBServerStatics;
import engine.server.world.WorldServer;
import engine.workthreads.DestroyCityThread;
import engine.workthreads.TransferCityThread;
import org.pmw.tinylog.Logger;

import java.sql.ResultSet;
import java.sql.SQLException;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.Iterator;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ThreadLocalRandom;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.locks.ReentrantReadWriteLock;

public class City extends AbstractWorldObject {

    public static long lastCityUpdate = 0;
    public final HashSet<Integer> _playerMemory = new HashSet<>();
    public java.time.LocalDateTime established;
    public boolean hasBeenTransfered = false;
    public LocalDateTime realmTaxDate;
    public ReentrantReadWriteLock transactionLock = new ReentrantReadWriteLock();
    public volatile boolean protectionEnforced = true;
    public ArrayList<Building> cityBarracks;
    public ArrayList<Integer> cityOutlaws = new ArrayList<>();
    public Zone parentZone;
    public int parentZoneUUID;
    private String cityName;
    private String motto;
    private String description;
    public int isNoobIsle; //1: noob, 0: not noob: -1: not noob, no teleport
    private int population = 0;
    private int siegesWithstood = 0;
    private int radiusType;
    private float bindRadius;
    private float bindX;
    private float bindZ;
    private byte isNpc;  //aka Safehold
    private byte isCapital = 0;
    private byte isSafeHold;
    private boolean forceRename = false;
    private boolean noTeleport = false; //used by npc cities
    private boolean noRepledge = false; //used by npc cities
    private int treeOfLifeID;
    private Vector3fImmutable location = Vector3fImmutable.ZERO;
    // Players who have entered the city (used for adding and removing affects)
    private Vector3fImmutable bindLoc;
    private boolean open = false;
    private String hash;
    public Warehouse warehouse;
    public Realm realm;
    public AtomicBoolean isDestroyed = new AtomicBoolean(false);

    /**
     * ResultSet Constructor
     */

    public City(ResultSet rs) throws SQLException {
        super(rs);
        try {
            this.parentZoneUUID = rs.getInt("parent");
            this.cityName = rs.getString("name");
            this.motto = rs.getString("motto");
            this.isNpc = rs.getByte("isNpc");
            this.isSafeHold = (byte) ((this.isNpc == 1) ? 1 : 0);
            this.description = ""; // TODO Implement this!
            this.isNoobIsle = rs.getByte("isNoobIsle"); // Noob
            this.gridObjectType = GridObjectType.STATIC;
            // Island
            // City(00000001),
            // Otherwise(FFFFFFFF)
            this.population = rs.getInt("population");
            this.siegesWithstood = rs.getInt("siegesWithstood");

            java.sql.Timestamp establishedTimeStamp = rs.getTimestamp("established");

            if (establishedTimeStamp != null)
                this.established = java.time.LocalDateTime.ofInstant(establishedTimeStamp.toInstant(), ZoneId.systemDefault());

            this.location = Vector3fImmutable.ZERO;

            java.sql.Timestamp realmTaxTimeStamp = rs.getTimestamp("realmTaxDate");

            if (realmTaxTimeStamp != null)
                this.realmTaxDate = realmTaxTimeStamp.toLocalDateTime();

            if (this.realmTaxDate == null)
                this.realmTaxDate = LocalDateTime.now();

            this.treeOfLifeID = rs.getInt("treeOfLifeUUID");
            this.bindX = rs.getFloat("bindX");
            this.bindZ = rs.getFloat("bindZ");
            this.bindLoc = new Vector3fImmutable(this.location.getX() + this.bindX, this.location.getY(), this.location.getZ() + this.bindZ);
            this.radiusType = rs.getInt("radiusType");

            float bindradiustemp = rs.getFloat("bindRadius");

            if (bindradiustemp > 2)
                bindradiustemp -= 2;

            this.bindRadius = bindradiustemp;

            this.forceRename = rs.getInt("forceRename") == 1;
            this.open = rs.getInt("open") == 1;

            this.hash = rs.getString("hash");

        } catch (Exception e) {
            Logger.error(e);
        }

    }

    /*
     * Utils
     */

    public static void _serializeForClientMsg(City city, ByteBufferWriter writer) {
        City.serializeForClientMsg(city, writer);
    }

    public static void serializeForClientMsg(City city, ByteBufferWriter writer) {
        AbstractCharacter guildRuler;
        Guild rulingGuild;
        Guild rulingNation;
        java.time.LocalDateTime dateTime1900;

        // Cities aren't a city without a TOL. Time to early exit.
        // No need to spam the log here as non-existant TOL's are indicated
        // during bootstrap routines.

        if (city.getTOL() == null) {

            Logger.error("NULL TOL FOR " + city.cityName);
        }

        // Assign city owner

        if (city.getTOL() != null)
            guildRuler = city.getTOL().getOwner();
        else
            guildRuler = null;

        // If is an errant tree, use errant guild for serialization.
        // otherwise we serialize the soverign guild

        if (guildRuler == null)
            rulingGuild = Guild.getErrantGuild();
        else
            rulingGuild = guildRuler.getGuild();

        rulingNation = rulingGuild.getNation();

        // Begin Serializing sovereign guild data

        writer.putInt(city.getObjectType().ordinal());
        writer.putInt(city.getObjectUUID());
        writer.putString(city.cityName);
        writer.putInt(rulingGuild.getObjectType().ordinal());
        writer.putInt(rulingGuild.getObjectUUID());

        writer.putString(rulingGuild.getName());
        writer.putString(city.motto);
        writer.putString(rulingGuild.getLeadershipType());

        // Serialize guild ruler's name
        // If tree is abandoned blank out the name
        // to allow them a rename.

        if (guildRuler == null)
            writer.putString("");
        else
            writer.putString(guildRuler.getFirstName() + ' ' + guildRuler.getLastName());

        writer.putInt(rulingGuild.charter.ordinal());
        writer.putInt(0); // always 00000000

        writer.put(city.isSafeHold);

        writer.put((byte) 1);
        writer.put((byte) 1);  // *** Refactor: What are these flags?
        writer.put((byte) 1);
        writer.put((byte) 1);
        writer.put((byte) 1);

        GuildTag._serializeForDisplay(rulingGuild.getGuildTag(), writer);
        GuildTag._serializeForDisplay(rulingNation.getGuildTag(), writer);

        writer.putInt(0);// TODO Implement description text

        writer.put((byte) 1);

        if (city.isCapital > 0)
            writer.put((byte) 1);
        else
            writer.put((byte) 0);

        writer.put((byte) 1);

        // Begin serializing nation guild info

        if (rulingNation.isEmptyGuild()) {
            writer.putInt(rulingGuild.getObjectType().ordinal());
            writer.putInt(rulingGuild.getObjectUUID());
        } else {
            writer.putInt(rulingNation.getObjectType().ordinal());
            writer.putInt(rulingNation.getObjectUUID());
        }

        // Serialize nation name

        if (rulingNation.isEmptyGuild())
            writer.putString("None");
        else
            writer.putString(rulingNation.getName());

        writer.putInt(city.getTOL().getRank());

        if (city.isNoobIsle > 0)
            writer.putInt(1);
        else
            writer.putInt(0xFFFFFFFF);

        writer.putInt(city.population);

        if (rulingNation.isEmptyGuild())
            writer.putString(" ");
        else
            writer.putString(Guild.GetGL(rulingNation).getFirstName() + ' ' + Guild.GetGL(rulingNation).getLastName());

        writer.putLocalDateTime(city.established);

        writer.putFloat(city.location.x);
        writer.putFloat(city.location.y);
        writer.putFloat(city.location.z);

        writer.putInt(city.siegesWithstood);

        writer.put((byte) 1);
        writer.put((byte) 0);
        writer.putInt(0x64);
        writer.put((byte) 0);
        writer.put((byte) 0);
        writer.put((byte) 0);
    }

    public static Vector3fImmutable getBindLoc(int cityID) {

        City city;

        city = City.getCity(cityID);

        if (city == null)
            return mbEnums.Ruins.getRandomRuin().getLocation();

        return city.getBindLoc();
    }

    public static ArrayList<City> getCitiesToTeleportTo(PlayerCharacter playerCharacter, boolean repledge) {

        ArrayList<City> cities = new ArrayList<>();

        if (playerCharacter == null)
            return cities;

        ConcurrentHashMap<Integer, AbstractGameObject> worldCities = DbManager.getMap(mbEnums.GameObjectType.City);

        //handle compiling of cities able to be teleported to for lore rule-set

        for (AbstractGameObject ago : worldCities.values()) {

            City city = (City) ago;

            // Filter Player cities

            if (city.parentZone == null)
                continue;

            // Can't teleport to something without a tree

            if (city.getTOL() == null)
                continue;

            // No abandoned cities

            if (city.getTOL().getGuild().isEmptyGuild())
                continue;

            // No destroyed cities

            if (city.getTOL().getRank() == -1)
                continue;

            //can't repledge to a guild you're already part of

            if (repledge && city.getGuild().equals(playerCharacter.guild))
                continue;

            if (city.parentZone.guild_zone) {

                //players can all port and repledge inside their own nation

                if(city.getGuild().getNation().equals(playerCharacter.guild.getNation())){
                    cities.add(city);
                    continue;
                }

                if (city.isOpen() && city.getTOL().rank > 4) {

                    // Filter Lore cities

                    if (ConfigManager.MB_RULESET.getValue().equals("LORE")) {

                        if (repledge) {
                            if (!city.getGuild().charter.canJoin(playerCharacter))
                                continue;
                        } else if (!city.getGuild().charter.equals(playerCharacter.guild.charter))
                                continue;
                    }

                    Integer playerUUID = playerCharacter.objectUUID;
                    Integer guildUUID = playerCharacter.guildUUID;
                    Integer nationUUID = playerCharacter.guild.getNation().getObjectUUID();
                    boolean allowed = false;

                    if (city.getTOL().reverseKOS) {

                        //reverse KOS, specific values are allowed

                        if ((city.getTOL().getCondemned().containsKey(playerUUID) && city.getTOL().getCondemned().get(playerUUID).active) ||
                                (city.getTOL().getCondemned().containsKey(guildUUID) && city.getTOL().getCondemned().get(guildUUID).active) ||
                                (city.getTOL().getCondemned().containsKey(nationUUID) && city.getTOL().getCondemned().get(nationUUID).active))
                            allowed = true;

                    } else {
                        //not reverse KOS, everyone is allowed by default

                        allowed = true;

                        //specific values are not allowed

                        if ((city.getTOL().getCondemned().containsKey(playerUUID) && city.getTOL().getCondemned().get(playerUUID).active) ||
                                (city.getTOL().getCondemned().containsKey(guildUUID) && city.getTOL().getCondemned().get(guildUUID).active) ||
                                (city.getTOL().getCondemned().containsKey(nationUUID) && city.getTOL().getCondemned().get(nationUUID).active))
                            allowed = false;

                    }

                    if (allowed)
                        cities.add(city);
                }
            } else {

                // Filter NPC cities

                if (city.isNoobIsle == 1) {

                    if (playerCharacter.level < 20)
                        cities.add(city); // everyone can go to noob island if they are under level 20

                    continue;
                }

                // Players cannot teleport to perdition or bastion

                if (city.cityName.equals("Perdition") || city.cityName.equals("Bastion"))
                    continue;

                // These cities are available for anyone off noob island

                if (playerCharacter.level >= 20 && (city.cityName.equals("Sea Dog's Rest") || city.cityName.equals("Khan'Ov Srekel") || city.cityName.equals("City of Khar Th'Sekt"))) {
                    cities.add(city);
                    continue;
                }

                // Add Lore cities

                if (ConfigManager.MB_RULESET.getValue().equals("LORE")) {

                    if (repledge) {
                        if (city.getGuild().charter.canJoin(playerCharacter))
                            cities.add(city);
                    } else if (city.getGuild().charter.equals(playerCharacter.guild.charter))
                        cities.add(city);
                }

            }
        }
        return cities;
    }

    public static ArrayList<City> getCitiesToRepledgeTo(PlayerCharacter playerCharacter) {

        ArrayList<City> cities = new ArrayList<>();

        if (playerCharacter == null)
            return cities;

        Guild pcG = playerCharacter.getGuild();

        ConcurrentHashMap<Integer, AbstractGameObject> worldCities = DbManager.getMap(mbEnums.GameObjectType.City);
        if (ConfigManager.MB_RULESET.getValue().equals("LORE")) {
            //handle compiling of cities able to be repledged to for lore rule-set
            for (AbstractGameObject ago : worldCities.values()) {
                City city = (City) ago;
                if (city.cityName.equals("Perdition") || city.cityName.equals("Bastion"))
                    continue; // cannot repledge to perdition or bastion
                if (city.isNpc == 1 && city.getGuild().charter.canJoin(playerCharacter)) {
                    cities.add(city); // anyone of the same charter can teleport to a safehold of that charter
                    continue;
                } else if (city.isNoobIsle == 1 && playerCharacter.level <= 20) {
                    cities.add(city); // everyone can go to noob island if they are under level 20
                    continue;
                } else if (city.isOpen() && city.getTOL().rank > 4 && city.getGuild().charter.canJoin(playerCharacter))
                    if (!city.getTOL().reverseKOS) {
                        cities.add(city);//can repledge to any open ToL that player can fit into charter
                        continue;
                    } else {
                        if (city.getTOL().getCondemned().contains(playerCharacter.objectUUID) && city.getTOL().getCondemned().get(playerCharacter.objectUUID).active) {
                            cities.add(city);//this player is allowed for the reverse KOS
                            continue;
                        }
                        if (city.getTOL().getCondemned().contains(playerCharacter.guildUUID) && city.getTOL().getCondemned().get(playerCharacter.guildUUID).active) {
                            cities.add(city);//this guild is allowed for the reverse KOS
                            continue;
                        }
                        if (city.getTOL().getCondemned().contains(playerCharacter.guild.getNation().getObjectUUID()) && city.getTOL().getCondemned().get(playerCharacter.guild.getNation().getObjectUUID()).active) {
                            cities.add(city);//this nation is allowed for the reverse KOS
                            continue;
                        }
                    }
                if (city.getGuild().getNation().equals(playerCharacter.guild.getNation())) {
                    cities.add(city);//can always teleport inside your own nation
                }
            }
        } else {
            //add npc cities

            for (AbstractGameObject ago : worldCities.values()) {
                if (ago.getObjectType().equals(GameObjectType.City)) {

                    City city = (City) ago;

                    if (city.noRepledge)
                        continue;

                    if (city.parentZone != null && city.parentZone.guild_zone) {

                        //list Player cities
                        //open city, just list

                        if (playerCharacter.getAccount().status.equals(AccountStatus.ADMIN)) {
                            cities.add(city);
                        } else if (city.open && city.getTOL() != null && city.getTOL().getRank() > 4) {

                            if (!BuildingManager.IsPlayerHostile(city.getTOL(), playerCharacter))
                                cities.add(city); //verify nation or guild is same
                        } else if (Guild.sameNationExcludeErrant(city.getGuild(), pcG))
                            cities.add(city);

                    } else if (city.isNpc == 1) {
                        //list NPC cities

                        Guild guild = city.getGuild();

                        if (guild == null) {
                            if (city.isNpc == 1)
                                if (city.isNoobIsle == 1) {
                                    if (playerCharacter.getLevel() < 21)
                                        cities.add(city); //verify nation or guild is same
                                } else if (playerCharacter.getLevel() > 9)
                                    cities.add(city); //verify nation or guild is same
                        } else if (playerCharacter.getLevel() >= guild.getRepledgeMin() && playerCharacter.getLevel() <= guild.getRepledgeMax()) {
                            cities.add(city); //verify nation or guild is same
                        }
                    }
                }
            }
        }
        return cities;
    }

    public static City getCity(int cityId) {

        if (cityId == 0)
            return null;

        City city = (City) DbManager.getFromCache(mbEnums.GameObjectType.City, cityId);

        if (city != null)
            return city;

        return DbManager.CityQueries.GET_CITY(cityId);

    }

    public static City GetCityFromCache(int cityId) {

        if (cityId == 0)
            return null;

        return (City) DbManager.getFromCache(mbEnums.GameObjectType.City, cityId);
    }

    public boolean renameCity(String cityName) {

        if (!DbManager.CityQueries.renameCity(this, cityName))
            return false;

        if (!DbManager.CityQueries.updateforceRename(this, false))
            return false;

        this.cityName = cityName;
        this.forceRename = false;
        return true;
    }

    public boolean updateTOL(Building tol) {

        if (tol == null)
            return false;

        if (!DbManager.CityQueries.updateTOL(this, tol.getObjectUUID()))
            return false;

        this.treeOfLifeID = tol.getObjectUUID();
        return true;
    }

    public boolean renameCityForNewPlant(String cityName) {

        if (!DbManager.CityQueries.renameCity(this, cityName))
            return false;

        if (!DbManager.CityQueries.updateforceRename(this, true))
            return false;

        this.cityName = cityName;
        this.forceRename = true;
        return true;
    }

    public String getCityName() {

        return cityName;
    }

    public String getMotto() {
        return motto;
    }

    public String getDescription() {
        return description;
    }

    public Building getTOL() {

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

        return BuildingManager.getBuildingFromCache(this.treeOfLifeID);
    }

    /**
     * @param population the population to set
     */
    public void setPopulation(int population) {
        this.population = population;
    }

    public int getSiegesWithstood() {
        return siegesWithstood;
    }

    /**
     * @param siegesWithstood the siegesWithstood to set
     */
    public void setSiegesWithstood(int siegesWithstood) {

        // early exit if setting to current value

        if (this.siegesWithstood == siegesWithstood)
            return;

        if (DbManager.CityQueries.updateSiegesWithstood(this, siegesWithstood))
            this.siegesWithstood = siegesWithstood;
        else
            Logger.error("Error when writing to database for cityUUID: " + this.getObjectUUID());
    }

    public float getAltitude() {
        return this.location.y;
    }

    @Override
    public Vector3fImmutable getLoc() {
        return this.location;
    }

    public boolean isSafeHold() {
        return (this.isSafeHold == (byte) 1);
    }

    public int getRank() {
        return (this.getTOL() == null) ? 0 : this.getTOL().getRank();
    }

    /*
     * Serializing
     */

    public Bane getBane() {
        return Bane.getBane(this.getObjectUUID());
    }

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

    public AbstractCharacter getOwner() {

        if (this.getTOL() == null)
            return null;

        int ownerID = this.getTOL().getOwnerUUID();

        if (ownerID == 0)
            return null;

        if (this.isNpc == 1)
            return NPC.getNPC(ownerID);
        else
            return PlayerCharacter.getPlayerCharacter(ownerID);
    }

    public Guild getGuild() {

        if (this.getTOL() == null)
            return Guild.getErrantGuild();

        if (this.getTOL().getOwner() == null)
            return Guild.getErrantGuild();

        return this.getTOL().getOwner().getGuild();
    }

    public boolean openCity(boolean open) {

        if (!DbManager.CityQueries.updateOpenCity(this, open))
            return false;

        this.open = open;
        return true;
    }

    public Vector3fImmutable getBindLoc() {

        Vector3fImmutable treeLoc = null;

        if (this.getTOL() != null && this.getTOL().getRank() == 8)
            treeLoc = this.getTOL().getStuckLocation();

        if (treeLoc != null)
            return treeLoc;

        if (this.radiusType == 1 && this.bindRadius > 0f) {

            //square radius

            float x = this.bindLoc.getX();
            float z = this.bindLoc.getZ();
            float offset = ((ThreadLocalRandom.current().nextFloat() * 2) - 1) * this.bindRadius;
            int direction = ThreadLocalRandom.current().nextInt(4);

            switch (direction) {
                case 0:
                    x += this.bindRadius;
                    z += offset;
                    break;
                case 1:
                    x += offset;
                    z -= this.bindRadius;
                    break;
                case 2:
                    x -= this.bindRadius;
                    z += offset;
                    break;
                case 3:
                    x += offset;
                    z += this.bindRadius;
                    break;
            }
            return new Vector3fImmutable(x, this.bindLoc.getY(), z);
        } else if (this.radiusType == 2 && this.bindRadius > 0f) {
            //circle radius
            Vector3fImmutable dir = FastMath.randomVector2D();
            return this.bindLoc.scaleAdd(this.bindRadius, dir);
        } else if (this.radiusType == 3 && this.bindRadius > 0f) {
            //random inside square
            float x = this.bindLoc.getX();
            x += ((ThreadLocalRandom.current().nextFloat() * 2) - 1) * this.bindRadius;
            float z = this.bindLoc.getZ();
            z += ((ThreadLocalRandom.current().nextFloat() * 2) - 1) * this.bindRadius;
            return new Vector3fImmutable(x, this.bindLoc.getY(), z);
        } else if (this.radiusType == 4 && this.bindRadius > 0f) {
            //random inside circle
            Vector3fImmutable dir = FastMath.randomVector2D();
            return this.bindLoc.scaleAdd(ThreadLocalRandom.current().nextFloat() * this.bindRadius, dir);
        } else
            //spawn at bindLoc
            //System.out.println("x: " + this.bindLoc.x + ", z: " + this.bindLoc.z);
            return this.bindLoc;
    }

    public NPC getRuneMaster() {
        NPC outNPC = null;

        if (this.getTOL() == null)
            return outNPC;

        for (AbstractCharacter npc : getTOL().getHirelings().keySet()) {
            if (npc.getObjectType() == GameObjectType.NPC)
                if (((NPC) npc).getContract().isRuneMaster())
                    outNPC = (NPC) npc;
        }

        return outNPC;
    }

    public boolean isOpen() {
        return open;
    }

    @Override
    public void updateDatabase() {
        // TODO Create update logic.
    }

    @Override
    public void runAfterLoad() {

        this.setObjectTypeMask(MBServerStatics.MASK_CITY);

        // Set parent

        this.parentZone = ZoneManager.getZoneByUUID(this.parentZoneUUID);

        // If it's not a player city then must be an NPC city

        if (!parentZone.guild_zone)
            parentZone.isNPCCity = true;

        // Set location for this city

        this.location = new Vector3fImmutable(this.parentZone.absX, this.parentZone.absY, this.parentZone.absZ);
        this.bindLoc = new Vector3fImmutable(this.location.x + this.bindX, this.location.y, this.location.z + this.bindZ);

        // set city bounds

        Bounds cityBounds = Bounds.borrow();
        cityBounds.setBounds(new Vector2f(this.location.x + 64, this.location.z + 64), // location x and z are offset by 64 from the center of the city.
                new Vector2f(mbEnums.CityBoundsType.GRID.halfExtents, mbEnums.CityBoundsType.GRID.halfExtents), 0.0f);
        this.setBounds(cityBounds);

        // Sanity check; no tol

        if (BuildingManager.getBuilding(this.treeOfLifeID) == null)
            Logger.info("City UID " + this.getObjectUUID() + " Failed to Load Tree of Life with ID " + this.treeOfLifeID);

        if ((ConfigManager.serverType.equals(ServerType.WORLDSERVER)) && (this.isNpc == (byte) 0)) {

            this.realm = RealmMap.getRealmAtLocation(this.getLoc());

            if (realm != null)
                realm.addCity(this.getObjectUUID());
            else
                Logger.error("Unable to find realm for city " + this.getObjectUUID());
        }

        // Set city motto to current guild motto

        if (this.getGuild() != null) {
            this.motto = this.getGuild().getMotto();

            // Determine if this city is a nation capitol

            if (this.getGuild().getGuildState() == GuildState.Nation)
                for (Guild sub : this.getGuild().getSubGuildList()) {

                    if ((sub.getGuildState() == GuildState.Protectorate) || (sub.getGuildState() == GuildState.Province)) {
                        this.isCapital = 1;
                        break;
                    }
                }

            ArrayList<PlayerCharacter> guildList = Guild.GuildRoster(this.getGuild());

            this.population = guildList.size();
        }

        if (this.cityName.equals("Perdition") || this.cityName.equals("Bastion")) {
            this.noTeleport = true;
            this.noRepledge = true;
        } else {
            this.noTeleport = false;
            this.noRepledge = false;
        }

        // Add city entry to data warehouse if newly created

        if ((ConfigManager.serverType.equals(mbEnums.ServerType.WORLDSERVER)) && (this.getHash() == null)) {

            this.setHash();

            if (!DataWarehouse.recordExists(DataRecordType.CITY, this.getObjectUUID())) {
                CityRecord cityRecord = CityRecord.borrow(this, mbEnums.RecordEventType.CREATE);
                DataWarehouse.pushToWarehouse(cityRecord);
            }
        }

        // Apply health bonus and special mesh for realm if applicable

        if (this.getTOL().rank == 8) {

            // Update mesh accordingly

            this.getTOL().meshUUID = Realm.getRealmMesh(this);

            // Apply realm capital health bonus.
            // Do not apply bonus to banestones or TOL's.  *** Refactor:
            // Possibly only protected buildings?  Needs some thought.

            float missingHealth = 0;

            if (this.health.get() != 0)
                missingHealth = this.healthMax - this.health.get();

            for (Building building : this.parentZone.zoneBuildingSet) {

                if (building.getBlueprint() != null && building.getBlueprint().getBuildingGroup() != BuildingGroup.BANESTONE && building.getBlueprint().getBuildingGroup() != BuildingGroup.TOL) {

                    building.healthMax += (building.healthMax * Realm.getRealmHealthMod(this));

                    if (this.health.get() != 0)
                        this.health.set(this.healthMax - missingHealth);

                    if (this.health.get() > this.healthMax)
                        this.health.set(this.healthMax);
                }

            }
        }

        // Banes are loaded for this city from the database at this point

        if (this.getBane() == null)
            return;

        // if this city is baned, add the siege effect

        this.getTOL().addEffectBit((1 << 16));
        this.getBane().getStone().addEffectBit((1 << 19));

        // Spawn city

        this.setLoc(this.getLoc());
    }

    public void addCityEffect(EffectsBase effectBase, int rank) {

        HashSet<AbstractWorldObject> currentPlayers;
        PlayerCharacter player;

        // Add this new effect to the current city effect collection.
        // so any new player to the grid will have all effects applied

        this.addEffectNoTimer(Integer.toString(effectBase.getUUID()), effectBase, rank, false);

        // Any players currently in the zone will not be processed by the heartbeat
        // if it's not the first effect toggled so we do it here manually

        currentPlayers = WorldGrid.getObjectsInRangePartial(this.location, this.parentZone.bounds.getHalfExtents().x * 1.2f, MBServerStatics.MASK_PLAYER);

        for (AbstractWorldObject playerObject : currentPlayers) {

            if (playerObject == null)
                continue;

            if (!this.isLocationWithinSiegeBounds(playerObject.getLoc()))
                continue;

            player = (PlayerCharacter) playerObject;
            player.addCityEffect(Integer.toString(effectBase.getUUID()), effectBase, rank, MBServerStatics.FOURTYFIVE_SECONDS, true, this);
        }

    }

    public void removeCityEffect(EffectsBase effectBase, int rank, boolean refreshEffect) {


        PlayerCharacter player;

        // Remove the city effect from the ago's internal collection

        this.getEffects().remove(Integer.toString(effectBase.getUUID()));

        // Any players currently in the zone will not be processed by the heartbeat
        // so we do it here manually


        for (Integer playerID : this._playerMemory) {

            player = PlayerCharacter.getFromCache(playerID);

            if (player == null)
                continue;

            player.endEffectNoPower(Integer.toString(effectBase.getUUID()));

            // Reapply effect with timeout?

            if (refreshEffect)
                player.addCityEffect(Integer.toString(effectBase.getUUID()), effectBase, rank, MBServerStatics.FOURTYFIVE_SECONDS, false, this);

        }
    }

    public boolean isLocationOnCityGrid(Vector3fImmutable insideLoc) {

        Bounds newBounds = Bounds.borrow();
        newBounds.setBounds(insideLoc);
        boolean collided = Bounds.collide(this.getBounds(), newBounds, 0);
        newBounds.release();
        return collided;
    }

    public boolean isLocationWithinSiegeBounds(Vector3fImmutable insideLoc) {

        return insideLoc.isInsideCircle(this.getLoc(), CityBoundsType.ZONE.halfExtents);

    }

    public boolean isLocationOnCityZone(Vector3fImmutable insideLoc) {
        return Bounds.collide(insideLoc, this.parentZone.bounds);
    }

    private void applyAllCityEffects(PlayerCharacter player) {

        Effect effect;
        EffectsBase effectBase;

        try {
            for (String cityEffect : this.getEffects().keySet()) {

                effect = this.getEffects().get(cityEffect);
                effectBase = effect.getEffectsBase();

                if (effectBase == null)
                    continue;

                player.addCityEffect(Integer.toString(effectBase.getUUID()), effectBase, effect.getTrains(), MBServerStatics.FOURTYFIVE_SECONDS, true, this);
            }
        } catch (Exception e) {
            Logger.error(e.getMessage());
        }

    }

    private void removeAllCityEffects(PlayerCharacter player, boolean force) {

        Effect effect;
        EffectsBase effectBase;

        try {
            for (String cityEffect : this.getEffects().keySet()) {

                effect = this.getEffects().get(cityEffect);
                effectBase = effect.getEffectsBase();

                if (player.getEffects().get(cityEffect) == null)
                    return;

                //                player.endEffectNoPower(cityEffect);
                player.addCityEffect(Integer.toString(effectBase.getUUID()), effectBase, effect.getTrains(), MBServerStatics.FOURTYFIVE_SECONDS, false, this);
            }
        } catch (Exception e) {
            Logger.error(e.getMessage());
        }
    }

    /* All characters in city player memory but
     * not the current memory have obviously
     * left the city.  Remove their affects.
     */

    public void onEnter() {

        HashSet<AbstractWorldObject> currentPlayers;
        HashSet<Integer> currentMemory;
        PlayerCharacter player;

        // Gather current list of players within the zone bounds

        currentPlayers = WorldGrid.getObjectsInRangePartial(this.location, CityBoundsType.ZONE.halfExtents, MBServerStatics.MASK_PLAYER);
        currentMemory = new HashSet<>();

        for (AbstractWorldObject playerObject : currentPlayers) {

            if (playerObject == null)
                continue;

            player = (PlayerCharacter) playerObject;
            currentMemory.add(player.getObjectUUID());

            // Player is already in our memory

            if (_playerMemory.contains(player.getObjectUUID()))
                continue;

            if (!this.isLocationWithinSiegeBounds(player.getLoc()))
                continue;
            // Apply safehold affect to player if needed

            if ((this.isSafeHold == 1))
                player.setSafeZone(true);

            //add spire effects.
            if (this.getEffects().size() > 0)
                this.applyAllCityEffects(player);

            // Add player to our city's memory

            _playerMemory.add(player.getObjectUUID());

            // ***For debugging
            // Logger.info("PlayerMemory for ", this.getCityName() + ": " + _playerMemory.size());
        }
        try {
            onExit(currentMemory);
        } catch (Exception e) {
            Logger.error(e.getMessage());
        }

    }

    private void onExit(HashSet<Integer> currentMemory) {

        PlayerCharacter player;
        int playerUUID = 0;
        HashSet<Integer> toRemove = new HashSet<>();
        Iterator<Integer> iter = _playerMemory.iterator();

        while (iter.hasNext()) {

            playerUUID = iter.next();


            player = PlayerCharacter.getFromCache(playerUUID);

            if (this.isLocationWithinSiegeBounds(player.getLoc()))
                continue;

            // Remove players safezone status if warranted
            // they can assumed to be not on the citygrid at
            // this point.


            player.setSafeZone(false);

            this.removeAllCityEffects(player, false);

            // We will remove this player after iteration is complete
            // so store it in a temporary collection

            toRemove.add(playerUUID);
            // ***For debugging
            // Logger.info("PlayerMemory for ", this.getCityName() + ": " + _playerMemory.size());
        }

        // Remove players from city memory

        _playerMemory.removeAll(toRemove);
        for (Integer removalUUID : toRemove) {
            this.cityOutlaws.remove(removalUUID);
        }
    }

    public final void destroy() {

        if (this.isDestroyed.compareAndSet(false, true)) {

            Thread destroyCityThread = new Thread(new DestroyCityThread(this));

            destroyCityThread.setName("destroyCity:" + this.getParent().zoneName);
            destroyCityThread.start();
        } else
            Logger.error("Attempt to destroy destroyed city");
    }

    public final void transfer(AbstractCharacter newOwner) {

        Thread transferCityThread = new Thread(new TransferCityThread(this, newOwner));

        transferCityThread.setName("TransferCity:" + this.getParent().zoneName);
        transferCityThread.start();
    }

    public final void claim(AbstractCharacter sourcePlayer) {

        Guild sourceNation;
        Guild sourceGuild;
        Zone cityZone;

        sourceGuild = sourcePlayer.getGuild();

        if (sourceGuild == null)
            return;

        sourceNation = sourcePlayer.getGuild().getNation();

        if (sourceGuild.isEmptyGuild())
            return;

        //cant claim tree with owned tree.

        if (sourceGuild.getOwnedCity() != null)
            return;

        cityZone = this.parentZone;

        // Can't claim a tree not in a player city zone

        // Reset sieges withstood

        this.setSiegesWithstood(0);

        this.hasBeenTransfered = true;

        // If currently a sub of another guild, desub when
        // claiming your new tree and set as Landed

        if (!sourceNation.isEmptyGuild() && sourceNation != sourceGuild) {
            if (!DbManager.GuildQueries.UPDATE_PARENT(sourceGuild.getObjectUUID(), WorldServer.worldUUID)) {
                ChatManager.chatGuildError((PlayerCharacter) sourcePlayer, "A Serious error has occurred. Please post details for to ensure transaction integrity");
                return;
            }

            sourceNation.getSubGuildList().remove(sourceGuild);

            if (sourceNation.getSubGuildList().isEmpty())
                sourceNation.downgradeGuildState();
        }

        // Link the mew guild with the tree

        if (!DbManager.GuildQueries.SET_GUILD_OWNED_CITY(sourceGuild.getObjectUUID(), this.getObjectUUID())) {
            ChatManager.chatGuildError((PlayerCharacter) sourcePlayer, "A Serious error has occurred. Please post details for to ensure transaction integrity");
            return;
        }

        sourceGuild.setCityUUID(this.getObjectUUID());

        sourceGuild.setNation(sourceGuild);
        sourceGuild.setGuildState(GuildState.Sovereign);
        GuildManager.updateAllGuildTags(sourceGuild);
        GuildManager.updateAllGuildBinds(sourceGuild, this);

        // Build list of buildings within this parent zone

        for (Building cityBuilding : cityZone.zoneBuildingSet) {

            // Buildings without blueprints are unclaimable

            if (cityBuilding.getBlueprintUUID() == 0)
                continue;

            // All protection contracts are void upon transfer of a city

            // All protection contracts are void upon transfer of a city
            //Dont forget to not Flip protection on Banestones and siege Equipment... Noob.

            if (cityBuilding.getBlueprint() != null && !cityBuilding.getBlueprint().isSiegeEquip() && cityBuilding.getBlueprint().getBuildingGroup() != BuildingGroup.BANESTONE)
                cityBuilding.setProtectionState(ProtectionState.NONE);

            // Transfer ownership of valid city assets
            // these assets are autoprotected.

            if ((cityBuilding.getBlueprint().getBuildingGroup() == BuildingGroup.TOL) || (cityBuilding.getBlueprint().getBuildingGroup() == BuildingGroup.SPIRE) || (cityBuilding.getBlueprint().getBuildingGroup() == BuildingGroup.BARRACK) || (cityBuilding.getBlueprint().isWallPiece()) || (cityBuilding.getBlueprint().getBuildingGroup() == BuildingGroup.SHRINE)) {
                PlayerCharacter guildLeader = PlayerCharacter.getPlayerCharacter(sourcePlayer.guild.getGuildLeaderUUID());
                if(guildLeader != null)
                    cityBuilding.claim(guildLeader);
                else
                    cityBuilding.claim(sourcePlayer);
                cityBuilding.setProtectionState(ProtectionState.PROTECTED);
            }
        }

        this.setForceRename(true);


        // Reset city timer for map update

        City.lastCityUpdate = System.currentTimeMillis();
    }

    public final boolean transferGuildLeader(PlayerCharacter sourcePlayer) {

        Guild sourceGuild;
        Zone cityZone;
        sourceGuild = sourcePlayer.getGuild();

        if (sourceGuild == null)
            return false;

        if (sourceGuild.isEmptyGuild())
            return false;

        cityZone = this.parentZone;

        for (Building cityBuilding : cityZone.zoneBuildingSet) {

            // Buildings without blueprints are unclaimable

            if (cityBuilding.getBlueprintUUID() == 0)
                continue;

            // All protection contracts are void upon transfer of a city
            //Dont forget to not Flip protection on Banestones and siege Equipment... Noob.

            // Transfer ownership of valid city assets
            // these assets are autoprotected.

            if ((cityBuilding.getBlueprint().getBuildingGroup() == BuildingGroup.TOL) || (cityBuilding.getBlueprint().getBuildingGroup() == BuildingGroup.SPIRE) || (cityBuilding.getBlueprint().getBuildingGroup() == BuildingGroup.BARRACK) || (cityBuilding.getBlueprint().isWallPiece()) || (cityBuilding.getBlueprint().getBuildingGroup() == BuildingGroup.SHRINE)) {

                cityBuilding.claim(sourcePlayer);
                cityBuilding.setProtectionState(ProtectionState.PROTECTED);
            } else if (cityBuilding.getBlueprint().getBuildingGroup() == BuildingGroup.WAREHOUSE)
                cityBuilding.claim(sourcePlayer);


        }
        this.setForceRename(true);
        CityRecord cityRecord = CityRecord.borrow(this, mbEnums.RecordEventType.TRANSFER);
        DataWarehouse.pushToWarehouse(cityRecord);
        return true;

    }

    /**
     * @return the forceRename
     */
    public boolean isForceRename() {
        return forceRename;
    }

    public void setForceRename(boolean forceRename) {
        if (!DbManager.CityQueries.updateforceRename(this, forceRename))
            return;
        this.forceRename = forceRename;
    }

    public String getHash() {
        return hash;
    }

    public void setHash(String hash) {
        this.hash = hash;
    }

    public void setHash() {

        this.hash = DataWarehouse.hasher.encrypt(this.getObjectUUID());

        // Write hash to player character table

        DataWarehouse.writeHash(mbEnums.DataRecordType.CITY, this.getObjectUUID());
    }

    public boolean setRealmTaxDate(LocalDateTime realmTaxDate) {

        if (!DbManager.CityQueries.updateRealmTaxDate(this, realmTaxDate))
            return false;

        this.realmTaxDate = realmTaxDate;
        return true;

    }

    public synchronized boolean TaxWarehouse(TaxResourcesMsg msg, PlayerCharacter player) {

        // Member variable declaration
        Building building = BuildingManager.getBuildingFromCache(msg.getBuildingID());
        Guild playerGuild = player.getGuild();

        if (building == null) {
            ErrorPopupMsg.sendErrorMsg(player, "Not a valid Building!");
            return true;
        }

        City city = building.getCity();

        if (city == null) {
            ErrorPopupMsg.sendErrorMsg(player, "This building does not belong to a city.");
            return true;
        }

        if (playerGuild == null || playerGuild.isEmptyGuild()) {
            ErrorPopupMsg.sendErrorMsg(player, "You must belong to a guild to do that!");
            return true;
        }

        if (playerGuild.getOwnedCity() == null) {
            ErrorPopupMsg.sendErrorMsg(player, "Your Guild needs to own a city!");
            return true;
        }

        if (playerGuild.getOwnedCity().getTOL() == null) {
            ErrorPopupMsg.sendErrorMsg(player, "Cannot find Tree of Life for your city!");
            return true;
        }

        if (playerGuild.getOwnedCity().getTOL().getRank() != 8) {
            ErrorPopupMsg.sendErrorMsg(player, "Your City needs to Own a realm!");
            return true;
        }

        if (RealmMap.getRealmIDAtLocation(playerGuild.getOwnedCity().getLoc()) == 0) {
            ErrorPopupMsg.sendErrorMsg(player, "Cannot find realm for your city!");
            return true;
        }

        Realm targetRealm = RealmMap.getRealmForCity(city);

        if (targetRealm == null) {
            ErrorPopupMsg.sendErrorMsg(player, "Cannot find realm for city you are attempting to tax!");
            return true;
        }

        if (targetRealm.getRulingCity() == null) {
            ErrorPopupMsg.sendErrorMsg(player, "Realm Does not have a ruling city!");
            return true;
        }

        if (targetRealm.getRulingCity().getObjectUUID() != playerGuild.getOwnedCity().getObjectUUID()) {
            ErrorPopupMsg.sendErrorMsg(player, "Your guild does not rule this realm!");
            return true;
        }

        if (playerGuild.getOwnedCity().getObjectUUID() == city.getObjectUUID()) {
            ErrorPopupMsg.sendErrorMsg(player, "You cannot tax your own city!");
            return true;
        }


        if (!GuildStatusController.isTaxCollector(player.getGuildStatus())) {
            ErrorPopupMsg.sendErrorMsg(player, "You Must be a tax Collector!");
            return true;
        }

        if (this.realmTaxDate.isAfter(LocalDateTime.now()))
            return true;

        if (msg.getResources().size() == 0)
            return true;

        if (city.warehouse == null)
            return true;

        Warehouse ruledWarehouse = playerGuild.getOwnedCity().warehouse;

        if (ruledWarehouse == null)
            return true;

        ArrayList<ResourceType> resources = new ArrayList<>();

        float taxPercent = msg.getTaxPercent();

        if (taxPercent > 20)
            taxPercent = .20f;

        for (int resourceHash : msg.getResources().keySet())
            resources.add(ResourceType.templateHashLookup.get(resourceHash));

        for (ResourceType resourceType : resources) {
            if (Warehouse.isAboveCap(ruledWarehouse, resourceType, (int) (city.warehouse.resources.get(resourceType) * taxPercent))) {
                ErrorPopupMsg.sendErrorMsg(player, "Your warehouse has enough " + resourceType.name() + " already!");
                return true;
            }

        }

        if (!city.setRealmTaxDate(LocalDateTime.now().plusDays(7))) {
            ErrorPopupMsg.sendErrorMsg(player, "Failed to Update next Tax Date due to internal Error. City was not charged taxes this time.");
            return false;
        }

        try {
            Warehouse.transferResources(city.warehouse, player, msg, resources, taxPercent);
        } catch (Exception e) {
            Logger.info(e.getMessage());
        }

        // Member variable assignment

        ViewResourcesMsg vrm = new ViewResourcesMsg(player);
        vrm.setGuild(building.getGuild());
        vrm.setWarehouseBuilding(BuildingManager.getBuildingFromCache(building.getCity().warehouse.building.getObjectUUID()));
        vrm.configure();
        Dispatch dispatch = Dispatch.borrow(player, vrm);
        DispatchManager.dispatchMsgDispatch(dispatch, mbEnums.DispatchChannel.SECONDARY);
        dispatch = Dispatch.borrow(player, msg);
        DispatchManager.dispatchMsgDispatch(dispatch, mbEnums.DispatchChannel.SECONDARY);
        return true;
    }

}