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


package engine.objects;

import engine.Enum;
import engine.InterestManagement.Terrain;
import engine.db.archive.DataWarehouse;
import engine.gameManager.DbManager;
import engine.gameManager.ZoneManager;
import engine.math.Bounds;
import engine.math.Vector2f;
import engine.math.Vector3fImmutable;
import engine.net.ByteBufferWriter;
import engine.server.MBServerStatics;
import org.pmw.tinylog.Logger;

import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;

public class Zone extends AbstractWorldObject {

    public final Set<Building> zoneBuildingSet = Collections.newSetFromMap(new ConcurrentHashMap<>());
    public final Set<NPC> zoneNPCSet = Collections.newSetFromMap(new ConcurrentHashMap<>());
    public final Set<Mob> zoneMobSet = Collections.newSetFromMap(new ConcurrentHashMap<>());
    public final int playerCityUUID;
    public final String zoneName;
    public final float xOffset;
    public final float zOffset;
    public final float yOffset;
    public final int template;
    public final byte peace_zone;
    public final String icon1;
    public final String icon2;
    public final String icon3;
    public float absX = 0.0f;
    public float absY = 0.0f;
    public float absZ = 0.0f;
    public int min_level;
    public int max_level;
    public boolean hasBeenHotzone = false;
    public ArrayList<Zone> nodes = null;
    public int parentZoneID;
    public Zone parent = null;
    public Bounds bounds;
    public boolean isNPCCity = false;
    public boolean guild_zone;
    public String hash;
    public float global_height = 0;
    public float seaLevel = 0f;
    public float sea_level_offset = 0;
    public static final Set<Mob> respawnQue = Collections.newSetFromMap(new ConcurrentHashMap<>());
    public static long lastRespawn = 0;
    public float major_radius;
    public float minor_radius;
    public float min_blend;
    public float max_blend;
    public String sea_level_type;
    public float sea_level;
    public String terrain_type;
    public float terrain_max_y;
    public int terrain_image;

    public Terrain terrain = null;

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

        super(rs);

            this.parentZoneID = rs.getInt("parent");
            this.playerCityUUID = rs.getInt("playerCityUUID");
            this.guild_zone = this.playerCityUUID != 0;
            this.zoneName = rs.getString("zone_name");
            this.xOffset = rs.getFloat("xOffset");
            this.zOffset = rs.getFloat("zOffset");
            this.yOffset = rs.getFloat("yOffset");
            this.template = rs.getInt("template");
            this.peace_zone = rs.getByte("peace_zone");
            this.icon1 = rs.getString("icon1");
            this.icon2 = rs.getString("icon2");
            this.icon3 = rs.getString("icon3");
            this.min_level = rs.getInt("min_level");
            this.max_level = rs.getInt("max_level");
        this.major_radius = rs.getFloat("major_radius");
        this.minor_radius = rs.getFloat("minor_radius");
        this.min_blend = rs.getFloat("min_blend");
        this.max_blend = rs.getFloat("max_blend");
        this.sea_level_type = rs.getString("sea_level_type");
        this.sea_level_offset = rs.getFloat("sea_level");
        this.terrain_type = rs.getString("terrain_type");
        this.terrain_max_y = rs.getFloat("terrain_max_y");
        this.terrain_image = rs.getInt("terrain_image");

        // Configuration for player cities

        if (this.guild_zone) {
            this.max_blend = 128;
            this.min_blend = 128;
            this.terrain_max_y = 5;
            this.major_radius = (int) Enum.CityBoundsType.ZONE.halfExtents;
            this.minor_radius = (int) Enum.CityBoundsType.ZONE.halfExtents;
            this.terrain_type = "PLANAR";
        }

        // If zone doesn't yet hava a hash then write it back to the zone table

        if (hash == null)
            setHash();

    }

    @Override
    public void runAfterLoad() {

        // First zone is always the seafloor

        if (ZoneManager.seaFloor == null)
            ZoneManager.seaFloor = this;

        if (this.terrain_type.equals("NONE"))
            this.terrain = null;
        else
            this.terrain = new Terrain(this);

        //this needs to be here specifically for new zones created after server boot (e.g. player city zones)

        Zone parentZone = ZoneManager.getZoneByUUID(parentZoneID);
        this.setParent(parentZone);

        if (this.min_level == 0 && parentZone != null) {
            this.min_level = parentZone.min_level;
            this.max_level = parentZone.max_level;
        }

        if (parentZone != null)
            parentZone.addNode(this);

        ZoneManager.populateZoneCollections(this);

    }

    public static void serializeForClientMsg(Zone zone, ByteBufferWriter writer) {

        if (zone.template == 0 && zone.playerCityUUID == 0)
            Logger.warn("Warning! WorldServerMap with ID " + zone.getObjectUUID() + " has a loadnum of 0 (player city) and no city linked. This will probably crash the client!");

        // Player City Terraform values serialized here.

        if (zone.playerCityUUID > 0) {
            writer.put((byte) 1); // Player City - True
            writer.putFloat(Enum.CityBoundsType.ZONE.halfExtents);
            writer.putFloat(Enum.CityBoundsType.ZONE.halfExtents);
        } else
            writer.put((byte) 0); // Player City - False

        writer.putFloat(zone.xOffset);
        writer.putFloat(zone.zOffset);
        writer.putFloat(zone.yOffset);

        writer.putInt(0);
        writer.putInt(0);
        writer.putInt(zone.template);

        if (zone.playerCityUUID > 0) {
            City k = City.getCity(zone.playerCityUUID);

            if (k != null) {
                writer.putInt(k.getObjectType().ordinal());
                writer.putInt(k.getObjectUUID());
            } else
                writer.putLong(0x0);
        } else {
            writer.putInt(zone.getObjectType().ordinal());
            writer.putInt(zone.getObjectUUID());
        }
        writer.putInt(zone.nodes.size());

        City city = City.getCity(zone.playerCityUUID);

        if (city != null)
            writer.putString(city.getCityName());
        else
            writer.putString(zone.zoneName);
        writer.put(zone.peace_zone);
        writer.putString(zone.icon1);
        writer.putString(zone.icon2);
        writer.putString(zone.icon3);
        writer.put((byte) 0); // Pad

        for (Zone child : zone.nodes) {
            Zone.serializeForClientMsg(child, writer);
        }
    }

    /* Method sets a default value for player cities
     * otherwise using values derived from the loadnum
     * field in the obj_zone database table.
     */
    public void setBounds() {

        // Set initial bounds object

        this.bounds = Bounds.borrow();
        this.bounds.setBounds(new Vector2f(this.absX, this.absZ), new Vector2f(this.major_radius, this.minor_radius), 0.0f);

    }

    public void setParent(final Zone value) {

        this.parent = value;
        this.parentZoneID = (this.parent != null) ? this.parent.getObjectUUID() : 0;

        // Seafloor

        if (this.parent == null) {
            this.absX = this.xOffset;
            this.absY = MBServerStatics.SEA_FLOOR_ALTITUDE;
            this.global_height = MBServerStatics.SEA_FLOOR_ALTITUDE;
            this.absZ = this.zOffset;
            this.seaLevel = 0;
            this.setBounds();
            return;
        }

        this.absX = this.xOffset + parent.absX;
        this.absY = this.yOffset + parent.absY;
        this.absZ = this.zOffset + parent.absZ;

        if (this.min_level == 0 || this.max_level == 0) {
            this.min_level = this.parent.min_level;
            this.max_level = this.parent.max_level;
        }

        this.setBounds();
        this.global_height = ZoneManager.calculateGlobalZoneHeight(this);
        setSeaLevel();
    }

    private void setSeaLevel() {

        int world_sea_level = 0;

        if (this.parent == null) {
            this.sea_level = world_sea_level;
            return;
        }

        switch (this.sea_level_type) {
            case "WORLD":
                this.sea_level = world_sea_level + this.sea_level_offset;
                break;
            case "PARENT":
                this.sea_level = this.parent.sea_level + this.sea_level_offset;
                break;
            case "SELF":
                this.sea_level = this.global_height + this.sea_level_offset;
                break;
        }
    }

    public boolean isMacroZone() {

        // Macro zones have icons.

        if (this.guild_zone == true)
            return false;

        if (this.parent == null)
            return false;

        return !icon1.equals("");
    }

    public Vector3fImmutable getLoc() {
        return new Vector3fImmutable(this.absX, this.absY, this.absZ);
    }

    public int getParentZoneID() {
        return this.parentZoneID;
    }

    public ArrayList<Zone> getNodes() {

        if (this.nodes == null) {
            this.nodes = DbManager.ZoneQueries.GET_MAP_NODES(super.getObjectUUID());

            //Add reverse lookup for child->parent
            if (this.nodes != null)
                for (Zone zone : this.nodes)
                    zone.setParent(this);
        }
        return nodes;
    }

    /*
     * Serializing
     */

    public void addNode(Zone child) {
        this.nodes.add(child);
    }

    @Override
    public void updateDatabase() {
        // TODO Auto-generated method stub
    }

    public boolean isContinent() {

        if (this.equals(ZoneManager.seaFloor))
            return false;

        if (this.getNodes().isEmpty())
            return false;

        if (this.getNodes().get(0).isMacroZone())
            return true;

        return this.parent.equals(ZoneManager.seaFloor);

    }
    public void setHash() {

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

        // Write hash to player character table

        DataWarehouse.writeHash(Enum.DataRecordType.ZONE, this.getObjectUUID());
    }

}