// 
// Decompiled by Procyon v0.6.0
// 

package com.hypixel.hytale.server.core.universe.world;

import it.unimi.dsi.fastutil.longs.LongCollection;
import java.util.function.Predicate;
import java.util.Set;
import java.util.Collection;
import java.util.HashSet;
import com.hypixel.hytale.component.Store;
import com.hypixel.hytale.protocol.packets.worldmap.UpdateWorldMapSettings;
import it.unimi.dsi.fastutil.longs.LongSet;
import com.hypixel.hytale.protocol.packets.worldmap.ClearWorldMap;
import java.util.Map;
import com.hypixel.hytale.protocol.Packet;
import com.hypixel.hytale.protocol.packets.worldmap.MapMarker;
import com.hypixel.hytale.protocol.packets.worldmap.UpdateWorldMap;
import com.hypixel.hytale.common.util.CompletableFutureUtil;
import java.util.Iterator;
import it.unimi.dsi.fastutil.longs.LongIterator;
import java.util.List;
import com.hypixel.hytale.protocol.packets.worldmap.MapChunk;
import it.unimi.dsi.fastutil.objects.ObjectArrayList;
import com.hypixel.hytale.math.util.ChunkUtil;
import com.hypixel.hytale.math.shape.Box2D;
import com.hypixel.hytale.protocol.SoundCategory;
import com.hypixel.hytale.server.core.asset.type.soundevent.config.SoundEvent;
import com.hypixel.hytale.server.core.util.EventTitleUtil;
import com.hypixel.hytale.server.core.Message;
import com.hypixel.hytale.server.core.universe.PlayerRef;
import com.hypixel.hytale.server.core.event.events.ecs.DiscoverZoneEvent;
import com.hypixel.hytale.component.ComponentAccessor;
import com.hypixel.hytale.server.core.universe.world.storage.EntityStore;
import com.hypixel.hytale.component.Ref;
import com.hypixel.hytale.math.vector.Vector3d;
import com.hypixel.hytale.server.core.universe.world.worldmap.WorldMapSettings;
import com.hypixel.hytale.server.core.universe.world.worldmap.WorldMapManager;
import com.hypixel.hytale.math.util.MathUtil;
import java.util.logging.Level;
import com.hypixel.hytale.common.fastutil.HLongOpenHashSet;
import javax.annotation.Nonnull;
import com.hypixel.hytale.server.core.modules.entity.component.TransformComponent;
import javax.annotation.Nullable;
import com.hypixel.hytale.server.core.universe.world.worldmap.markers.MapMarkerTracker;
import com.hypixel.hytale.protocol.packets.worldmap.MapImage;
import java.util.concurrent.CompletableFuture;
import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap;
import com.hypixel.hytale.common.fastutil.HLongSet;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import com.hypixel.hytale.math.iterator.CircleSpiralIterator;
import com.hypixel.hytale.server.core.entity.entities.Player;
import com.hypixel.hytale.logger.HytaleLogger;
import com.hypixel.hytale.common.thread.ticking.Tickable;

public class WorldMapTracker implements Tickable
{
    private static final HytaleLogger LOGGER;
    public static final float UPDATE_SPEED = 1.0f;
    public static final int RADIUS_MAX = 512;
    public static final int EMPTY_UPDATE_WORLD_MAP_SIZE = 13;
    private static final int EMPTY_MAP_CHUNK_SIZE = 10;
    private static final int FULL_MAP_CHUNK_SIZE = 23;
    public static final int MAX_IMAGE_GENERATION = 20;
    public static final int MAX_FRAME = 2621440;
    private final Player player;
    private final CircleSpiralIterator spiralIterator;
    private final ReentrantReadWriteLock loadedLock;
    private final HLongSet loaded;
    private final HLongSet pendingReloadChunks;
    private final Long2ObjectOpenHashMap<CompletableFuture<MapImage>> pendingReloadFutures;
    private final MapMarkerTracker markerTracker;
    private float updateTimer;
    private Integer viewRadiusOverride;
    private boolean started;
    private int sentViewRadius;
    private int lastChunkX;
    private int lastChunkZ;
    @Nullable
    private String currentBiomeName;
    @Nullable
    private ZoneDiscoveryInfo currentZone;
    private boolean clientHasWorldMapVisible;
    @Nullable
    private TransformComponent transformComponent;
    static final /* synthetic */ boolean $assertionsDisabled;
    
    public WorldMapTracker(@Nonnull final Player player) {
        this.spiralIterator = new CircleSpiralIterator();
        this.loadedLock = new ReentrantReadWriteLock();
        this.loaded = new HLongOpenHashSet();
        this.pendingReloadChunks = new HLongOpenHashSet();
        this.pendingReloadFutures = new Long2ObjectOpenHashMap<CompletableFuture<MapImage>>();
        this.player = player;
        this.markerTracker = new MapMarkerTracker(this);
    }
    
    @Override
    public void tick(final float dt) {
        if (!this.started) {
            this.started = true;
            WorldMapTracker.LOGGER.at(Level.INFO).log("Started Generating Map!");
        }
        final World world = this.player.getWorld();
        if (world == null) {
            return;
        }
        if (this.transformComponent == null) {
            this.transformComponent = this.player.getTransformComponent();
            if (this.transformComponent == null) {
                return;
            }
        }
        final WorldMapManager worldMapManager = world.getWorldMapManager();
        final WorldMapSettings worldMapSettings = worldMapManager.getWorldMapSettings();
        int viewRadius;
        if (this.viewRadiusOverride != null) {
            viewRadius = this.viewRadiusOverride;
        }
        else {
            viewRadius = worldMapSettings.getViewRadius(this.player.getViewRadius());
        }
        final Vector3d position = this.transformComponent.getPosition();
        final int playerX = MathUtil.floor(position.getX());
        final int playerZ = MathUtil.floor(position.getZ());
        final int playerChunkX = playerX >> 5;
        final int playerChunkZ = playerZ >> 5;
        if (world.isCompassUpdating()) {
            this.markerTracker.updatePointsOfInterest(dt, world, viewRadius, playerChunkX, playerChunkZ);
        }
        if (worldMapManager.isWorldMapEnabled()) {
            this.updateWorldMap(world, dt, worldMapSettings, viewRadius, playerChunkX, playerChunkZ);
        }
    }
    
    public void updateCurrentZoneAndBiome(@Nonnull final Ref<EntityStore> ref, @Nullable final ZoneDiscoveryInfo zoneDiscoveryInfo, @Nullable final String biomeName, @Nonnull final ComponentAccessor<EntityStore> componentAccessor) {
        this.currentBiomeName = biomeName;
        this.currentZone = zoneDiscoveryInfo;
        final Player playerComponent = componentAccessor.getComponent(ref, Player.getComponentType());
        assert playerComponent != null;
        if (!playerComponent.isWaitingForClientReady()) {
            final World world = componentAccessor.getExternalData().getWorld();
            if (zoneDiscoveryInfo != null && this.discoverZone(world, zoneDiscoveryInfo.regionName())) {
                this.onZoneDiscovered(ref, zoneDiscoveryInfo, componentAccessor);
            }
        }
    }
    
    private void onZoneDiscovered(@Nonnull final Ref<EntityStore> ref, @Nonnull final ZoneDiscoveryInfo zoneDiscoveryInfo, @Nonnull final ComponentAccessor<EntityStore> componentAccessor) {
        final ZoneDiscoveryInfo discoverZoneEventInfo = zoneDiscoveryInfo.clone();
        final DiscoverZoneEvent.Display discoverZoneEvent = new DiscoverZoneEvent.Display(discoverZoneEventInfo);
        componentAccessor.invoke(ref, discoverZoneEvent);
        if (discoverZoneEvent.isCancelled() || !discoverZoneEventInfo.display()) {
            return;
        }
        final PlayerRef playerRefComponent = componentAccessor.getComponent(ref, PlayerRef.getComponentType());
        assert playerRefComponent != null;
        EventTitleUtil.showEventTitleToPlayer(playerRefComponent, Message.translation(String.format("server.map.region.%s", discoverZoneEventInfo.regionName())), Message.translation(String.format("server.map.zone.%s", discoverZoneEventInfo.zoneName())), discoverZoneEventInfo.major(), discoverZoneEventInfo.icon(), discoverZoneEventInfo.duration(), discoverZoneEventInfo.fadeInDuration(), discoverZoneEventInfo.fadeOutDuration());
        final String discoverySoundEventId = discoverZoneEventInfo.discoverySoundEventId();
        if (discoverySoundEventId != null) {
            final int assetIndex = SoundEvent.getAssetMap().getIndex(discoverySoundEventId);
            if (assetIndex != Integer.MIN_VALUE) {
                SoundUtil.playSoundEvent2d(ref, assetIndex, SoundCategory.UI, componentAccessor);
            }
        }
    }
    
    private void updateWorldMap(@Nonnull final World world, final float dt, @Nonnull final WorldMapSettings worldMapSettings, final int chunkViewRadius, final int playerChunkX, final int playerChunkZ) {
        this.processPendingReloadChunks(world);
        final Box2D worldMapArea = worldMapSettings.getWorldMapArea();
        if (worldMapArea == null) {
            final int xDiff = Math.abs(this.lastChunkX - playerChunkX);
            final int zDiff = Math.abs(this.lastChunkZ - playerChunkZ);
            final int chunkMoveDistance = (xDiff > 0 || zDiff > 0) ? ((int)Math.ceil(Math.sqrt(xDiff * xDiff + zDiff * zDiff))) : 0;
            this.sentViewRadius = Math.max(0, this.sentViewRadius - chunkMoveDistance);
            this.lastChunkX = playerChunkX;
            this.lastChunkZ = playerChunkZ;
            this.updateTimer -= dt;
            if (this.updateTimer > 0.0f) {
                return;
            }
            if (this.sentViewRadius != chunkViewRadius) {
                if (this.sentViewRadius > chunkViewRadius) {
                    this.sentViewRadius = chunkViewRadius;
                }
                this.unloadImages(chunkViewRadius, playerChunkX, playerChunkZ);
                if (this.sentViewRadius < chunkViewRadius) {
                    this.loadImages(world, chunkViewRadius, playerChunkX, playerChunkZ, 20);
                }
            }
            else {
                this.updateTimer = 1.0f;
            }
        }
        else {
            this.updateTimer -= dt;
            if (this.updateTimer > 0.0f) {
                return;
            }
            this.loadWorldMap(world, worldMapArea, 20);
        }
    }
    
    private void unloadImages(final int chunkViewRadius, final int playerChunkX, final int playerChunkZ) {
        List<MapChunk> currentUnloadList = null;
        List<List<MapChunk>> allUnloadLists = null;
        this.loadedLock.writeLock().lock();
        try {
            int packetSize = 2621427;
            final LongIterator iterator = this.loaded.iterator();
            while (iterator.hasNext()) {
                final long chunkCoordinates = iterator.nextLong();
                final int mapChunkX = ChunkUtil.xOfChunkIndex(chunkCoordinates);
                final int mapChunkZ = ChunkUtil.zOfChunkIndex(chunkCoordinates);
                if (shouldBeVisible(chunkViewRadius, playerChunkX, playerChunkZ, mapChunkX, mapChunkZ)) {
                    continue;
                }
                if (currentUnloadList == null) {
                    currentUnloadList = new ObjectArrayList<MapChunk>(packetSize / 10);
                }
                currentUnloadList.add(new MapChunk(mapChunkX, mapChunkZ, null));
                packetSize -= 10;
                iterator.remove();
                if (packetSize >= 10) {
                    continue;
                }
                packetSize = 2621427;
                if (allUnloadLists == null) {
                    allUnloadLists = new ObjectArrayList<List<MapChunk>>(this.loaded.size() / (packetSize / 10));
                }
                allUnloadLists.add(currentUnloadList);
                currentUnloadList = new ObjectArrayList<MapChunk>(packetSize / 10);
            }
            if (allUnloadLists != null) {
                for (final List<MapChunk> unloadList : allUnloadLists) {
                    this.writeUpdatePacket(unloadList);
                }
            }
            this.writeUpdatePacket(currentUnloadList);
        }
        finally {
            this.loadedLock.writeLock().unlock();
        }
    }
    
    private void processPendingReloadChunks(@Nonnull final World world) {
        List<MapChunk> chunksToSend = null;
        this.loadedLock.writeLock().lock();
        try {
            if (this.pendingReloadChunks.isEmpty()) {
                return;
            }
            final int imageSize = MathUtil.fastFloor(32.0f * world.getWorldMapManager().getWorldMapSettings().getImageScale());
            final int fullMapChunkSize = 23 + 4 * imageSize * imageSize;
            int packetSize = 2621427;
            final LongIterator iterator = this.pendingReloadChunks.iterator();
            while (iterator.hasNext()) {
                final long chunkCoordinates = iterator.nextLong();
                CompletableFuture<MapImage> future = this.pendingReloadFutures.get(chunkCoordinates);
                if (future == null) {
                    future = world.getWorldMapManager().getImageAsync(chunkCoordinates);
                    this.pendingReloadFutures.put(chunkCoordinates, future);
                }
                if (!future.isDone()) {
                    continue;
                }
                iterator.remove();
                this.pendingReloadFutures.remove(chunkCoordinates);
                if (chunksToSend == null) {
                    chunksToSend = new ObjectArrayList<MapChunk>(packetSize / fullMapChunkSize);
                }
                final int mapChunkX = ChunkUtil.xOfChunkIndex(chunkCoordinates);
                final int mapChunkZ = ChunkUtil.zOfChunkIndex(chunkCoordinates);
                chunksToSend.add(new MapChunk(mapChunkX, mapChunkZ, future.getNow(null)));
                this.loaded.add(chunkCoordinates);
                packetSize -= fullMapChunkSize;
                if (packetSize >= fullMapChunkSize) {
                    continue;
                }
                this.writeUpdatePacket(chunksToSend);
                chunksToSend = new ObjectArrayList<MapChunk>(2621440 - 13 / fullMapChunkSize);
                packetSize = 2621427;
            }
            this.writeUpdatePacket(chunksToSend);
        }
        finally {
            this.loadedLock.writeLock().unlock();
        }
    }
    
    private int loadImages(@Nonnull final World world, final int chunkViewRadius, final int playerChunkX, final int playerChunkZ, int maxGeneration) {
        List<MapChunk> currentLoadList = null;
        List<List<MapChunk>> allLoadLists = null;
        this.loadedLock.writeLock().lock();
        try {
            int packetSize = 2621427;
            final int imageSize = MathUtil.fastFloor(32.0f * world.getWorldMapManager().getWorldMapSettings().getImageScale());
            final int fullMapChunkSize = 23 + 4 * imageSize * imageSize;
            boolean areAllLoaded = true;
            this.spiralIterator.init(playerChunkX, playerChunkZ, this.sentViewRadius, chunkViewRadius);
            while (maxGeneration > 0 && this.spiralIterator.hasNext()) {
                final long chunkCoordinates = this.spiralIterator.next();
                if (!this.loaded.contains(chunkCoordinates)) {
                    areAllLoaded = false;
                    final CompletableFuture<MapImage> future = world.getWorldMapManager().getImageAsync(chunkCoordinates);
                    if (!future.isDone()) {
                        --maxGeneration;
                    }
                    else {
                        if (!this.loaded.add(chunkCoordinates)) {
                            continue;
                        }
                        if (currentLoadList == null) {
                            currentLoadList = new ObjectArrayList<MapChunk>(packetSize / fullMapChunkSize);
                        }
                        final int mapChunkX = ChunkUtil.xOfChunkIndex(chunkCoordinates);
                        final int mapChunkZ = ChunkUtil.zOfChunkIndex(chunkCoordinates);
                        currentLoadList.add(new MapChunk(mapChunkX, mapChunkZ, future.getNow(null)));
                        packetSize -= fullMapChunkSize;
                        if (packetSize >= fullMapChunkSize) {
                            continue;
                        }
                        packetSize = 2621427;
                        if (allLoadLists == null) {
                            allLoadLists = new ObjectArrayList<List<MapChunk>>();
                        }
                        allLoadLists.add(currentLoadList);
                        currentLoadList = new ObjectArrayList<MapChunk>(packetSize / fullMapChunkSize);
                    }
                }
                else {
                    if (!areAllLoaded) {
                        continue;
                    }
                    this.sentViewRadius = this.spiralIterator.getCompletedRadius();
                }
            }
            if (areAllLoaded) {
                this.sentViewRadius = this.spiralIterator.getCompletedRadius();
            }
            if (allLoadLists != null) {
                for (final List<MapChunk> unloadList : allLoadLists) {
                    this.writeUpdatePacket(unloadList);
                }
            }
            this.writeUpdatePacket(currentLoadList);
        }
        finally {
            this.loadedLock.writeLock().unlock();
        }
        return maxGeneration;
    }
    
    private int loadWorldMap(@Nonnull final World world, @Nonnull final Box2D worldMapArea, int maxGeneration) {
        List<MapChunk> currentLoadList = null;
        List<List<MapChunk>> allLoadLists = null;
        this.loadedLock.writeLock().lock();
        try {
            int packetSize = 2621427;
            final int imageSize = MathUtil.fastFloor(32.0f * world.getWorldMapManager().getWorldMapSettings().getImageScale());
            final int fullMapChunkSize = 23 + 4 * imageSize * imageSize;
            for (int mapChunkX = MathUtil.floor(worldMapArea.min.x); mapChunkX < MathUtil.ceil(worldMapArea.max.x) && maxGeneration > 0; ++mapChunkX) {
                for (int mapChunkZ = MathUtil.floor(worldMapArea.min.y); mapChunkZ < MathUtil.ceil(worldMapArea.max.y) && maxGeneration > 0; ++mapChunkZ) {
                    final long chunkCoordinates = ChunkUtil.indexChunk(mapChunkX, mapChunkZ);
                    if (!this.loaded.contains(chunkCoordinates)) {
                        final CompletableFuture<MapImage> future = CompletableFutureUtil._catch(world.getWorldMapManager().getImageAsync(chunkCoordinates));
                        if (!future.isDone()) {
                            --maxGeneration;
                        }
                        else {
                            if (currentLoadList == null) {
                                currentLoadList = new ObjectArrayList<MapChunk>(packetSize / fullMapChunkSize);
                            }
                            currentLoadList.add(new MapChunk(mapChunkX, mapChunkZ, future.getNow(null)));
                            this.loaded.add(chunkCoordinates);
                            packetSize -= fullMapChunkSize;
                            if (packetSize < fullMapChunkSize) {
                                packetSize = 2621427;
                                if (allLoadLists == null) {
                                    allLoadLists = new ObjectArrayList<List<MapChunk>>(Math.max(packetSize / fullMapChunkSize, 1));
                                }
                                allLoadLists.add(currentLoadList);
                                currentLoadList = new ObjectArrayList<MapChunk>(packetSize / fullMapChunkSize);
                            }
                        }
                    }
                }
            }
        }
        finally {
            this.loadedLock.writeLock().unlock();
        }
        if (allLoadLists != null) {
            for (final List<MapChunk> unloadList : allLoadLists) {
                this.writeUpdatePacket(unloadList);
            }
        }
        this.writeUpdatePacket(currentLoadList);
        return maxGeneration;
    }
    
    private void writeUpdatePacket(@Nullable final List<MapChunk> list) {
        if (list != null) {
            final UpdateWorldMap packet = new UpdateWorldMap(list.toArray(MapChunk[]::new), null, null);
            WorldMapTracker.LOGGER.at(Level.FINE).log("Sending world map update to %s - %d chunks", this.player.getUuid(), list.size());
            this.player.getPlayerConnection().write(packet);
        }
    }
    
    @Nonnull
    public Map<String, MapMarker> getSentMarkers() {
        return this.markerTracker.getSentMarkers();
    }
    
    @Nonnull
    public Player getPlayer() {
        return this.player;
    }
    
    @Nullable
    public TransformComponent getTransformComponent() {
        return this.transformComponent;
    }
    
    public void clear() {
        this.loadedLock.writeLock().lock();
        try {
            this.loaded.clear();
            this.sentViewRadius = 0;
            this.markerTracker.getSentMarkers().clear();
        }
        finally {
            this.loadedLock.writeLock().unlock();
        }
        this.player.getPlayerConnection().write(new ClearWorldMap());
    }
    
    public void clearChunks(@Nonnull final LongSet chunkIndices) {
        this.loadedLock.writeLock().lock();
        try {
            chunkIndices.forEach(index -> {
                this.loaded.remove(index);
                this.pendingReloadChunks.add(index);
                this.pendingReloadFutures.remove(index);
                return;
            });
        }
        finally {
            this.loadedLock.writeLock().unlock();
        }
        this.updateTimer = 0.0f;
    }
    
    public void sendSettings(@Nonnull final World world) {
        final UpdateWorldMapSettings worldMapSettingsPacket = new UpdateWorldMapSettings(world.getWorldMapManager().getWorldMapSettings().getSettingsPacket());
        world.execute(() -> {
            final Store<EntityStore> store = world.getEntityStore().getStore();
            final Ref<EntityStore> ref = this.player.getReference();
            if (ref != null) {
                final Player playerComponent = store.getComponent(ref, Player.getComponentType());
                if (!WorldMapTracker.$assertionsDisabled && playerComponent == null) {
                    throw new AssertionError();
                }
                else {
                    final PlayerRef playerRefComponent = store.getComponent(ref, PlayerRef.getComponentType());
                    if (!WorldMapTracker.$assertionsDisabled && playerRefComponent == null) {
                        throw new AssertionError();
                    }
                    else {
                        worldMapSettingsPacket.allowTeleportToCoordinates = this.isAllowTeleportToCoordinates();
                        worldMapSettingsPacket.allowTeleportToMarkers = this.isAllowTeleportToMarkers();
                        playerRefComponent.getPacketHandler().write(worldMapSettingsPacket);
                    }
                }
            }
        });
    }
    
    private boolean hasDiscoveredZone(@Nonnull final String zoneName) {
        return this.player.getPlayerConfigData().getDiscoveredZones().contains(zoneName);
    }
    
    public boolean discoverZone(@Nonnull final World world, @Nonnull final String zoneName) {
        Set<String> discoveredZones = this.player.getPlayerConfigData().getDiscoveredZones();
        if (!discoveredZones.contains(zoneName)) {
            discoveredZones = new HashSet<String>(discoveredZones);
            discoveredZones.add(zoneName);
            this.player.getPlayerConfigData().setDiscoveredZones(discoveredZones);
            this.sendSettings(world);
            return true;
        }
        return false;
    }
    
    public boolean undiscoverZone(@Nonnull final World world, @Nonnull final String zoneName) {
        Set<String> discoveredZones = this.player.getPlayerConfigData().getDiscoveredZones();
        if (discoveredZones.contains(zoneName)) {
            discoveredZones = new HashSet<String>(discoveredZones);
            discoveredZones.remove(zoneName);
            this.player.getPlayerConfigData().setDiscoveredZones(discoveredZones);
            this.sendSettings(world);
            return true;
        }
        return false;
    }
    
    public boolean discoverZones(@Nonnull final World world, @Nonnull final Set<String> zoneNames) {
        Set<String> discoveredZones = this.player.getPlayerConfigData().getDiscoveredZones();
        if (!discoveredZones.containsAll(zoneNames)) {
            discoveredZones = new HashSet<String>(discoveredZones);
            discoveredZones.addAll(zoneNames);
            this.player.getPlayerConfigData().setDiscoveredZones(discoveredZones);
            this.sendSettings(world);
            return true;
        }
        return false;
    }
    
    public boolean undiscoverZones(@Nonnull final World world, @Nonnull final Set<String> zoneNames) {
        Set<String> discoveredZones = this.player.getPlayerConfigData().getDiscoveredZones();
        if (discoveredZones.containsAll(zoneNames)) {
            discoveredZones = new HashSet<String>(discoveredZones);
            discoveredZones.removeAll(zoneNames);
            this.player.getPlayerConfigData().setDiscoveredZones(discoveredZones);
            this.sendSettings(world);
            return true;
        }
        return false;
    }
    
    public boolean isAllowTeleportToCoordinates() {
        return this.player.hasPermission("hytale.world_map.teleport.coordinate");
    }
    
    public boolean isAllowTeleportToMarkers() {
        return this.player.hasPermission("hytale.world_map.teleport.marker");
    }
    
    public void setPlayerMapFilter(final Predicate<PlayerRef> playerMapFilter) {
        this.markerTracker.setPlayerMapFilter(playerMapFilter);
    }
    
    public void setClientHasWorldMapVisible(final boolean visible) {
        this.clientHasWorldMapVisible = visible;
    }
    
    @Nullable
    public Integer getViewRadiusOverride() {
        return this.viewRadiusOverride;
    }
    
    @Nullable
    public String getCurrentBiomeName() {
        return this.currentBiomeName;
    }
    
    @Nullable
    public ZoneDiscoveryInfo getCurrentZone() {
        return this.currentZone;
    }
    
    public void setViewRadiusOverride(@Nullable final Integer viewRadiusOverride) {
        this.viewRadiusOverride = viewRadiusOverride;
        this.clear();
    }
    
    public int getEffectiveViewRadius(@Nonnull final World world) {
        if (this.viewRadiusOverride != null) {
            return this.viewRadiusOverride;
        }
        return world.getWorldMapManager().getWorldMapSettings().getViewRadius(this.player.getViewRadius());
    }
    
    public boolean shouldBeVisible(final int chunkViewRadius, final long chunkCoordinates) {
        if (this.player == null || this.transformComponent == null) {
            return false;
        }
        final Vector3d position = this.transformComponent.getPosition();
        final int chunkX = MathUtil.floor(position.getX()) >> 5;
        final int chunkZ = MathUtil.floor(position.getZ()) >> 5;
        final int x = ChunkUtil.xOfChunkIndex(chunkCoordinates);
        final int z = ChunkUtil.zOfChunkIndex(chunkCoordinates);
        return shouldBeVisible(chunkViewRadius, chunkX, chunkZ, x, z);
    }
    
    public void copyFrom(@Nonnull final WorldMapTracker worldMapTracker) {
        this.loadedLock.writeLock().lock();
        try {
            worldMapTracker.loadedLock.readLock().lock();
            try {
                this.loaded.addAll(worldMapTracker.loaded);
                this.markerTracker.copyFrom(worldMapTracker.markerTracker);
            }
            finally {
                worldMapTracker.loadedLock.readLock().unlock();
            }
        }
        finally {
            this.loadedLock.writeLock().unlock();
        }
    }
    
    public static boolean shouldBeVisible(final int chunkViewRadius, final int chunkX, final int chunkZ, final int x, final int z) {
        final int xDiff = Math.abs(x - chunkX);
        final int zDiff = Math.abs(z - chunkZ);
        final int distanceSq = xDiff * xDiff + zDiff * zDiff;
        return distanceSq <= chunkViewRadius * chunkViewRadius;
    }
    
    static {
        LOGGER = HytaleLogger.forEnclosingClass();
    }
    
    record ZoneDiscoveryInfo(@Nonnull String zoneName, @Nonnull String regionName, boolean display, @Nullable String discoverySoundEventId, @Nullable String icon, boolean major, float duration, float fadeInDuration, float fadeOutDuration) {
        @Nonnull
        public ZoneDiscoveryInfo clone() {
            return new ZoneDiscoveryInfo(this.zoneName, this.regionName, this.display, this.discoverySoundEventId, this.icon, this.major, this.duration, this.fadeInDuration, this.fadeOutDuration);
        }
        
        @Nonnull
        public String zoneName() {
            return this.zoneName;
        }
        
        @Nonnull
        public String regionName() {
            return this.regionName;
        }
        
        @Nullable
        public String discoverySoundEventId() {
            return this.discoverySoundEventId;
        }
        
        @Nullable
        public String icon() {
            return this.icon;
        }
    }
}
