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

package engine.gameManager;

import engine.InterestManagement.Terrain;
import engine.math.Bounds;
import engine.math.Vector2f;
import engine.math.Vector3f;
import engine.math.Vector3fImmutable;
import engine.mbEnums;
import engine.objects.Building;
import engine.objects.City;
import engine.objects.Zone;
import engine.objects.ZoneTemplate;
import engine.server.MBServerStatics;
import org.pmw.tinylog.Logger;

import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ThreadLocalRandom;

/*
 * Class contains methods and structures which
 * track in-game Zones
 */
public enum ZoneManager {

    ZONEMANAGER;

    public static HashMap<Integer, ZoneTemplate> _zone_templates = new HashMap<>();

    public static final Set<Zone> macroZones = Collections.newSetFromMap(new ConcurrentHashMap<>());
    private static final ConcurrentHashMap<Integer, Zone> zonesByID = new ConcurrentHashMap<>(MBServerStatics.CHM_INIT_CAP, MBServerStatics.CHM_LOAD);
    private static final ConcurrentHashMap<Integer, Zone> zonesByUUID = new ConcurrentHashMap<>(MBServerStatics.CHM_INIT_CAP, MBServerStatics.CHM_LOAD);
    private static final ConcurrentHashMap<String, Zone> zonesByName = new ConcurrentHashMap<>(MBServerStatics.CHM_INIT_CAP, MBServerStatics.CHM_LOAD);
    private static final Set<Zone> npcCityZones = Collections.newSetFromMap(new ConcurrentHashMap<>());
    private static final Set<Zone> playerCityZones = Collections.newSetFromMap(new ConcurrentHashMap<>());
    public static Instant hotZoneLastUpdate;
    public static Zone hotZone = null;
    public static int hotZoneCycle = 0;  // Used with HOTZONE_DURATION from config.

    /* Instance variables */
    public static Zone seaFloor = null;

    // Find all zones coordinates fit into, starting with Sea Floor

    public static ArrayList<Zone> getAllZonesIn(final Vector3fImmutable loc) {

        ArrayList<Zone> allIn = new ArrayList<>();
        Zone zone;

        zone = ZoneManager.findSmallestZone(loc);

        if (zone != null) {
            allIn.add(zone);
            while (zone.parent != null) {
                zone = zone.parent;
                allIn.add(zone);
            }
        }
        return allIn;
    }

    // Find smallest zone coordinates fit into.

    public static Zone findSmallestZone(final Vector3fImmutable loc) {

        Zone zone = ZoneManager.seaFloor;

        if (zone == null)
            return null;

        boolean childFound = true;

        while (childFound) {

            childFound = false;

            ArrayList<Zone> nodes = zone.nodes;

            // Logger.info("soze", "" + nodes.size());
            if (nodes != null)
                for (Zone child : nodes) {

                    if (Bounds.collide(loc, child.bounds)) {
                        zone = child;
                        childFound = true;
                        break;
                    }
                }
        }
        return zone;
    }

    // Returns the number of available hotZones
    // remaining in this cycle (1am)

    public static int availableHotZones() {

        int count = 0;

        for (Zone zone : ZoneManager.macroZones)
            if (ZoneManager.validHotZone(zone))
                count = count + 1;

        return count;
    }

    // Resets the  availability of hotZones
    // for this cycle

    public static void resetHotZones() {

        for (Zone zone : ZoneManager.macroZones)
            if (zone.wasHotzonw)
                zone.wasHotzonw = false;

    }

    public static Zone getZoneByUUID(final int zoneUUID) {
        return ZoneManager.zonesByUUID.get(zoneUUID);
    }

    public static Zone getZoneByZoneID(final int zoneID) {

        return ZoneManager.zonesByID.get(zoneID);
    }

    public static Zone getZoneByName(final String zoneName) {
        return ZoneManager.zonesByName.get(zoneName);
    }

    public static Collection<Zone> getAllZones() {
        return ZoneManager.zonesByUUID.values();
    }

    public static void setHotZone(final Zone zone) {

        if (!zone.isMacroZone())
            return;

        ZoneManager.hotZone = zone;
        ZoneManager.hotZoneCycle = 1;  // Used with HOTZONE_DURATION from config.
        zone.wasHotzonw = true;
        ZoneManager.hotZoneLastUpdate = LocalDateTime.now().withMinute(0).withSecond(0).atZone(ZoneId.systemDefault()).toInstant();

    }

    public static boolean inHotZone(final Vector3fImmutable loc) {

        if (ZoneManager.hotZone == null)
            return false;

        return (Bounds.collide(loc, ZoneManager.hotZone.bounds));
    }

    public static void populateZoneCollections(final Zone zone) {

        // Zones are added to separate
        // collections for quick access
        // based upon their type.

        ZoneManager.zonesByID.put(zone.templateID, zone);

        ZoneManager.zonesByUUID.put(zone.getObjectUUID(), zone);

        ZoneManager.zonesByName.put(zone.zoneName.toLowerCase(), zone);

        if (zone.isMacroZone()) {
            addMacroZone(zone);
            return;
        }

        if (zone.guild_zone) {
            ZoneManager.playerCityZones.add(zone);
            return;
        }

        if (zone.isNPCCity)
            addNPCCityZone(zone);

    }

    private static void addMacroZone(final Zone zone) {
        ZoneManager.macroZones.add(zone);
    }

    private static void addNPCCityZone(final Zone zone) {
        zone.isNPCCity = true;
        ZoneManager.npcCityZones.add(zone);
    }

    public static final void generateAndSetRandomHotzone() {

        Zone hotZone;
        ArrayList<Integer> zoneArray = new ArrayList<>();

        if (ZoneManager.macroZones.isEmpty())
            return;

        // Reset hotZone availability if none are left.

        if (ZoneManager.availableHotZones() == 0)
            ZoneManager.resetHotZones();

        for (Zone zone : ZoneManager.macroZones)
            if (validHotZone(zone))
                zoneArray.add(zone.getObjectUUID());

        int entryIndex = ThreadLocalRandom.current().nextInt(zoneArray.size());

        hotZone = ZoneManager.getZoneByUUID(zoneArray.get(entryIndex));

        if (hotZone == null) {
            Logger.error("Hotzone is null");
            return;
        }

        ZoneManager.setHotZone(hotZone);

    }

    public static final boolean validHotZone(Zone zone) {

        if (zone.peace_zone == (byte) 1)
            return false; // no safe zone hotzones// if (this.hotzone == null)

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

        if (zone.nodes.isEmpty())
            return false;

        //no duplicate hotZones

        if (zone.wasHotzonw == true)
            return false;

        // Enforce min level

        if (zone.min_level < Integer.parseInt(ConfigManager.MB_HOTZONE_MIN_LEVEL.getValue()))
            return false;

        if (ZoneManager.hotZone != null)
            return ZoneManager.hotZone.getObjectUUID() != zone.getObjectUUID();

        return true;
    }

    // Converts world coordinates to coordinates local to a given zone.

    public static Vector3fImmutable worldToLocal(Vector3fImmutable worldVector,
                                                 Zone zone) {

        Vector3fImmutable localCoords;

        localCoords = new Vector3fImmutable(worldVector.x - zone.absX,
                worldVector.y - zone.absY, worldVector.z
                - zone.absZ);

        return localCoords;
    }

    public static Vector2f worldToZoneOffset(Vector3fImmutable worldLoc,
                                             Zone zone) {

        Vector2f zoneLoc;

        zoneLoc = new Vector2f(worldLoc.x, worldLoc.z).subtract(zone.getLoc().x, zone.getLoc().z);
        zoneLoc.y *= -1;

        return zoneLoc;

    }

    public static Vector2f worldToTerrainSpace(Vector3fImmutable worldLoc,
                                               Zone zone) {

        Vector2f localCoords;
        Vector2f zoneOrigin;

        // Top left corner of zone is calculated in world space by the center and it's extents.

        zoneOrigin = new Vector2f(zone.getLoc().x, zone.getLoc().z);
        zoneOrigin = zoneOrigin.subtract(new Vector2f(zone.bounds.getHalfExtents().x, zone.bounds.getHalfExtents().y));

        // Local coordinate in world space translated to an offset from the calculated zone origin.

        localCoords = new Vector2f(worldLoc.x, worldLoc.z);
        localCoords = localCoords.subtract(zoneOrigin);

        return localCoords;
    }

    // Converts local zone coordinates to world coordinates


    /**
     * Converts from local (relative to this building) to world.
     *
     * @param localPos position in local reference (relative to this building)
     * @return position relative to world
     */

    public static Vector3fImmutable convertLocalToWorld(Building building, Vector3fImmutable localPos) {

        // convert from SB rotation value to radians

        if (building.getBounds().getQuaternion() == null)
            return building.getLoc();

        // handle building rotation

        Vector3fImmutable rotatedLocal = Vector3fImmutable.rotateAroundPoint(Vector3fImmutable.ZERO, localPos, building.getBounds().getQuaternion());

        // handle building translation

        return building.getLoc().add(rotatedLocal.x, rotatedLocal.y, rotatedLocal.z);
    }


    //used for regions, Building bounds not set yet.
    public static Vector3f convertLocalToWorld(Building building, Vector3f localPos, Bounds bounds) {

        // handle building rotation

        Vector3f rotatedLocal = Vector3f.rotateAroundPoint(Vector3f.ZERO, localPos, bounds.getQuaternion());

        // handle building translation

        return new Vector3f(building.getLoc().add(rotatedLocal.x, rotatedLocal.y, rotatedLocal.z));
    }

    public static Vector3fImmutable convertWorldToLocal(Building building, Vector3fImmutable WorldPos) {
        Vector3fImmutable convertLoc = Vector3fImmutable.rotateAroundPoint(building.getLoc(), WorldPos, -building.getBounds().getQuaternion().angleY);

        convertLoc = convertLoc.subtract(building.getLoc());

        // convert from SB rotation value to radians

        return convertLoc;

    }

    // Method returns a city if the given location is within
    // a city zone.

    public static City getCityAtLocation(Vector3fImmutable worldLoc) {

        Zone currentZone;

        currentZone = ZoneManager.findSmallestZone(worldLoc);

        if (currentZone.guild_zone)
            return City.getCity(currentZone.playerCityUUID);

        return null;
    }

    /* Method is called when creating a new player city to
     * validate that the new zone does not overlap any other
     * zone that might currently exist
     */

    public static boolean validTreePlacementLoc(Zone currentZone, float positionX, float positionZ) {

        // Member Variable declaration

        ArrayList<Zone> zoneList;
        boolean validLocation = true;
        Bounds treeBounds;

        if (currentZone.isContinent() == false)
            return false;


        treeBounds = Bounds.borrow();
        treeBounds.setBounds(new Vector2f(positionX, positionZ), new Vector2f(mbEnums.CityBoundsType.PLACEMENT.halfExtents, mbEnums.CityBoundsType.PLACEMENT.halfExtents), 0.0f);

        zoneList = currentZone.nodes;

        for (Zone zone : zoneList) {

            if (zone.isContinent())
                continue;

            if (Bounds.collide(treeBounds, zone.bounds, 0.0f))
                validLocation = false;
        }

        treeBounds.release();
        return validLocation;
    }

    public static float calculateGlobalZoneHeight(Zone zone) {

        float worldAltitude = MBServerStatics.SEA_FLOOR_ALTITUDE;

        // Seafloor

        if (ZoneManager.seaFloor.equals(zone))
            return worldAltitude;

        // Children of seafloor

        if (ZoneManager.seaFloor.equals(zone.parent))
            return worldAltitude + zone.yOffset;

        // return height from heightmap engine at zone location

        worldAltitude = Terrain.getWorldHeight(zone.parent, zone.getLoc());

        // Add zone offset to value

        worldAltitude += zone.yOffset;

        return worldAltitude;
    }

    public static boolean isLocUnderwater(Vector3fImmutable currentLoc) {

        float localAltitude = Terrain.getWorldHeight(currentLoc);
        Zone zone = findSmallestZone(currentLoc);

        return localAltitude < zone.sea_level;
    }
}
