package com.hypixel.hytale.server.npc.role.support;

import com.hypixel.hytale.assetstore.map.BlockTypeAssetMap;
import com.hypixel.hytale.component.CommandBuffer;
import com.hypixel.hytale.component.ComponentAccessor;
import com.hypixel.hytale.component.ComponentType;
import com.hypixel.hytale.component.Ref;
import com.hypixel.hytale.function.consumer.DoubleQuadObjectConsumer;
import com.hypixel.hytale.function.consumer.QuadConsumer;
import com.hypixel.hytale.function.consumer.TriConsumer;
import com.hypixel.hytale.function.predicate.QuadPredicate;
import com.hypixel.hytale.math.iterator.BlockIterator;
import com.hypixel.hytale.math.random.RandomExtra;
import com.hypixel.hytale.math.shape.Box;
import com.hypixel.hytale.math.util.ChunkUtil;
import com.hypixel.hytale.math.util.MathUtil;
import com.hypixel.hytale.math.vector.Vector2d;
import com.hypixel.hytale.math.vector.Vector3d;
import com.hypixel.hytale.protocol.GameMode;
import com.hypixel.hytale.protocol.Opacity;
import com.hypixel.hytale.server.core.asset.type.blocktype.config.BlockType;
import com.hypixel.hytale.server.core.entity.entities.Player;
import com.hypixel.hytale.server.core.modules.blockset.BlockSetModule;
import com.hypixel.hytale.server.core.modules.collision.CollisionMath;
import com.hypixel.hytale.server.core.modules.entity.component.BoundingBox;
import com.hypixel.hytale.server.core.modules.entity.component.ModelComponent;
import com.hypixel.hytale.server.core.modules.entity.component.TransformComponent;
import com.hypixel.hytale.server.core.modules.entity.item.ItemComponent;
import com.hypixel.hytale.server.core.modules.entity.player.PlayerSettings;
import com.hypixel.hytale.server.core.universe.world.World;
import com.hypixel.hytale.server.core.universe.world.chunk.WorldChunk;
import com.hypixel.hytale.server.core.universe.world.storage.EntityStore;
import com.hypixel.hytale.server.npc.NPCPlugin;
import com.hypixel.hytale.server.npc.entities.NPCEntity;
import com.hypixel.hytale.server.npc.role.Role;
import com.hypixel.hytale.server.npc.util.NPCPhysicsMath;
import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
import it.unimi.dsi.fastutil.ints.IntSet;
import it.unimi.dsi.fastutil.objects.Object2ByteMap;
import it.unimi.dsi.fastutil.objects.Object2ByteOpenHashMap;
import it.unimi.dsi.fastutil.objects.ObjectArrayList;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.function.BiPredicate;
import java.util.function.Consumer;
import java.util.function.Predicate;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;

public class PositionCache {
   public static final BiPredicate<Ref<EntityStore>, ComponentAccessor<EntityStore>> IS_VALID_PLAYER = (ref, componentAccessor) -> {
      Player playerComponent = componentAccessor.getComponent(ref, Player.getComponentType());
      if (playerComponent != null && !playerComponent.isWaitingForClientReady()) {
         if (playerComponent.getGameMode() == GameMode.Adventure) {
            return true;
         } else if (playerComponent.getGameMode() != GameMode.Creative) {
            return false;
         } else {
            PlayerSettings playerSettingsComponent = componentAccessor.getComponent(ref, PlayerSettings.getComponentType());
            return playerSettingsComponent != null && playerSettingsComponent.creativeSettings().allowNPCDetection();
         }
      } else {
         return false;
      }
   };
   public static final BiPredicate<Ref<EntityStore>, ComponentAccessor<EntityStore>> IS_VALID_NPC = (ref, accessor) -> accessor.getArchetype(ref)
      .contains(NPCEntity.getComponentType());
   public static final double MIN_LOS_BLOCKING_DISTANCE_SQUARED = 1.0E-6;
   public static final String FUNCTION_CAN_BE_ONLY_CALLED_WHILE_CONFIGURING_POSITION_CACHE = "function can be only called while configuring PositionCache";
   private static final float LOS_CACHE_TTL_MIN_SECONDS = 0.09F;
   private static final float LOS_CACHE_TTL_MAX_SECONDS = 0.11F;
   private static final float POSITION_CACHE_TTL_SECONDS = 0.2F;
   private static final ComponentType<EntityStore, TransformComponent> TRANSFORM_COMPONENT_TYPE = TransformComponent.getComponentType();
   private static final ComponentType<EntityStore, ItemComponent> ITEM_COMPONENT_TYPE = ItemComponent.getComponentType();
   private static final ComponentType<EntityStore, ModelComponent> MODEL_COMPONENT_TYPE = ModelComponent.getComponentType();
   protected static final ComponentType<EntityStore, BoundingBox> BOUNDING_BOX_COMPONENT_TYPE = BoundingBox.getComponentType();
   private double maxDroppedItemDistance;
   private double maxSpawnMarkerDistance;
   private int maxSpawnBeaconDistance;
   @Nonnull
   private final Role role;
   private int opaqueBlockSet;
   protected EntityList players;
   protected EntityList npcs;
   protected final List<Consumer<Role>> externalRegistrations = new ObjectArrayList<>();
   private final List<Ref<EntityStore>> droppedItems = new ObjectArrayList<>();
   private final List<Ref<EntityStore>> spawnMarkers = new ObjectArrayList<>();
   private final List<Ref<EntityStore>> spawnBeacons = new ObjectArrayList<>();
   private final Object2ByteMap<Ref<EntityStore>> lineOfSightCache = new Object2ByteOpenHashMap<>();
   private final Object2ByteMap<Ref<EntityStore>> inverseLineOfSightCache = new Object2ByteOpenHashMap<>();
   private final Object2ByteMap<Ref<EntityStore>> friendlyFireCache = new Object2ByteOpenHashMap<>();
   protected final PositionCache.LineOfSightBuffer lineOfSightComputeBuffer = new PositionCache.LineOfSightBuffer();
   protected final PositionCache.LineOfSightEntityBuffer lineOfSightEntityComputeBuffer = new PositionCache.LineOfSightEntityBuffer();
   private float cacheTTL = 0.09F;
   private float positionCacheNextUpdate;
   private boolean isBenchmarking;
   private boolean isConfiguring;
   private boolean couldBreathe = true;

   public PositionCache(@Nonnull Role role) {
      this.role = role;
      this.players = new EntityList(null, IS_VALID_PLAYER);
      this.npcs = new EntityList(null, IS_VALID_NPC);
   }

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

   public void setBenchmarking(boolean benchmarking) {
      this.isBenchmarking = benchmarking;
   }

   public void setCouldBreathe(boolean couldBreathe) {
      this.couldBreathe = couldBreathe;
   }

   public EntityList getPlayers() {
      return this.players;
   }

   public EntityList getNpcs() {
      return this.npcs;
   }

   public boolean tickPositionCacheNextUpdate(float dt) {
      return (this.positionCacheNextUpdate -= dt) <= 0.0F;
   }

   public void resetPositionCacheNextUpdate() {
      this.positionCacheNextUpdate = 0.2F;
   }

   public double getMaxDroppedItemDistance() {
      return this.maxDroppedItemDistance;
   }

   public double getMaxSpawnMarkerDistance() {
      return this.maxSpawnMarkerDistance;
   }

   public int getMaxSpawnBeaconDistance() {
      return this.maxSpawnBeaconDistance;
   }

   public void addExternalPositionCacheRegistration(Consumer<Role> registration) {
      this.externalRegistrations.add(registration);
   }

   @Nonnull
   public List<Consumer<Role>> getExternalRegistrations() {
      return this.externalRegistrations;
   }

   public void reset(boolean isConfiguring) {
      this.players.reset();
      this.npcs.reset();
      this.maxDroppedItemDistance = 0.0;
      this.droppedItems.clear();
      this.spawnMarkers.clear();
      this.spawnBeacons.clear();
      this.positionCacheNextUpdate = RandomExtra.randomRange(0.0F, 0.2F);
      this.clearLineOfSightCache();
      this.isConfiguring = isConfiguring;
   }

   public void finalizeConfiguration() {
      this.isConfiguring = false;
      this.npcs.finalizeConfiguration();
      this.players.finalizeConfiguration();
      RoleStats roleStats = this.role.getRoleStats();
      if (roleStats != null) {
         roleStats.trackBuckets(false, this.npcs.getBucketRanges());
         roleStats.trackBuckets(true, this.players.getBucketRanges());
      }
   }

   public void clear(double tickTime) {
      this.clearLineOfSightCache(tickTime);
      if (this.isBenchmarking) {
         NPCPlugin.get().collectSensorSupportTickDone(this.role.getRoleIndex());
      }

      this.isBenchmarking = false;
   }

   public boolean couldBreatheCached() {
      return this.couldBreathe;
   }

   public <T, U, V> void forEachPlayer(
      @Nonnull DoubleQuadObjectConsumer<Ref<EntityStore>, T, U, V> consumer, T t, U u, V v, double d, ComponentAccessor<EntityStore> componentAccessor
   ) {
      this.players.forEachEntity(consumer, t, u, v, d, componentAccessor);
   }

   @Nullable
   public Ref<EntityStore> getClosestPlayerInRange(double minRange, double maxRange, @Nonnull ComponentAccessor<EntityStore> componentAccessor) {
      return this.getClosestPlayerInRange(minRange, maxRange, p -> true, componentAccessor);
   }

   @Nullable
   public Ref<EntityStore> getClosestPlayerInRange(
      double minRange, double maxRange, @Nonnull Predicate<Ref<EntityStore>> filter, @Nonnull ComponentAccessor<EntityStore> componentAccessor
   ) {
      return this.players.getClosestEntityInRange(minRange, maxRange, filter, componentAccessor);
   }

   @Nullable
   public Ref<EntityStore> getClosestNPCInRange(
      double minRange, double maxRange, @Nonnull Predicate<Ref<EntityStore>> filter, @Nonnull ComponentAccessor<EntityStore> componentAccessor
   ) {
      return this.npcs.getClosestEntityInRange(minRange, maxRange, filter, componentAccessor);
   }

   public <S, T> void processNPCsInRange(
      @Nonnull Ref<EntityStore> ref,
      double minRange,
      double maxRange,
      boolean useProjectedDistance,
      Ref<EntityStore> ignoredEntityReference,
      @Nonnull Role role,
      @Nonnull QuadPredicate<S, Ref<EntityStore>, Role, T> filter,
      S s,
      T t,
      @Nonnull ComponentAccessor<EntityStore> componentAccessor
   ) {
      this.processEntitiesInRange(ref, this.npcs, minRange, maxRange, useProjectedDistance, ignoredEntityReference, role, filter, s, t, componentAccessor);
   }

   public <S, T> void processPlayersInRange(
      @Nonnull Ref<EntityStore> ref,
      double minRange,
      double maxRange,
      boolean useProjectedDistance,
      Ref<EntityStore> ignoredEntityReference,
      @Nonnull Role role,
      @Nonnull QuadPredicate<S, Ref<EntityStore>, Role, T> filter,
      S s,
      T t,
      @Nonnull ComponentAccessor<EntityStore> componentAccessor
   ) {
      this.processEntitiesInRange(ref, this.players, minRange, maxRange, useProjectedDistance, ignoredEntityReference, role, filter, s, t, componentAccessor);
   }

   public <S, T> void processEntitiesInRange(
      @Nonnull Ref<EntityStore> ref,
      @Nonnull EntityList entities,
      double minRange,
      double maxRange,
      boolean useProjectedDistance,
      Ref<EntityStore> ignoredEntityReference,
      @Nonnull Role role,
      @Nonnull QuadPredicate<S, Ref<EntityStore>, Role, T> filter,
      S s,
      T t,
      @Nonnull ComponentAccessor<EntityStore> componentAccessor
   ) {
      if (useProjectedDistance) {
         entities.getClosestEntityInRangeProjected(
            ref, ignoredEntityReference, role.getActiveMotionController(), minRange, maxRange, filter, role, s, t, componentAccessor
         );
      } else {
         entities.getClosestEntityInRange(ignoredEntityReference, minRange, maxRange, filter, role, s, t, componentAccessor);
      }
   }

   @Nullable
   public <S> Ref<EntityStore> getClosestDroppedItemInRange(
      @Nonnull Ref<EntityStore> ref,
      double minRange,
      double maxRange,
      @Nonnull QuadPredicate<S, Ref<EntityStore>, Role, ComponentAccessor<EntityStore>> filter,
      Role role,
      S s,
      @Nonnull ComponentAccessor<EntityStore> componentAccessor
   ) {
      int droppedItemsSize = this.droppedItems.size();
      if (droppedItemsSize == 0) {
         return null;
      } else {
         TransformComponent transformComponent = componentAccessor.getComponent(ref, TRANSFORM_COMPONENT_TYPE);

         assert transformComponent != null;

         Vector3d position = transformComponent.getPosition();
         minRange *= minRange;
         maxRange *= maxRange;

         for (int index = 0; index < droppedItemsSize; index++) {
            Ref<EntityStore> itemEntityRef = this.droppedItems.get(index);
            if (itemEntityRef.isValid()) {
               ItemComponent itemComponent = componentAccessor.getComponent(itemEntityRef, ITEM_COMPONENT_TYPE);
               if (itemComponent != null) {
                  TransformComponent itemEntityTransformComponent = componentAccessor.getComponent(itemEntityRef, TRANSFORM_COMPONENT_TYPE);

                  assert itemEntityTransformComponent != null;

                  double squaredDistance = itemEntityTransformComponent.getPosition().distanceSquaredTo(position);
                  if (!(squaredDistance < minRange)) {
                     if (squaredDistance >= maxRange) {
                        break;
                     }

                     if (filter.test(s, itemEntityRef, role, componentAccessor)) {
                        return itemEntityRef;
                     }
                  }
               }
            }
         }

         return null;
      }
   }

   public <S> boolean isEntityCountInRange(
      double minRange,
      double maxRange,
      int minCount,
      int maxCount,
      boolean findPlayers,
      Role role,
      @Nonnull QuadPredicate<S, Ref<EntityStore>, Role, ComponentAccessor<EntityStore>> filter,
      S s,
      ComponentAccessor<EntityStore> componentAccessor
   ) {
      int count = 0;
      if (findPlayers) {
         count = this.players.countEntitiesInRange(minRange, maxRange, maxCount + 1, filter, s, role, componentAccessor);
         if (count > maxCount) {
            return false;
         }
      }

      count += this.npcs.countEntitiesInRange(minRange, maxRange, maxCount - count + 1, filter, s, role, componentAccessor);
      return count >= minCount && count <= maxCount;
   }

   public <S, T> int countEntitiesInRange(
      double minRange,
      double maxRange,
      boolean findPlayers,
      @Nonnull QuadPredicate<S, Ref<EntityStore>, T, ComponentAccessor<EntityStore>> filter,
      S s,
      T t,
      ComponentAccessor<EntityStore> componentAccessor
   ) {
      int count = 0;
      if (findPlayers) {
         count = this.players.countEntitiesInRange(minRange, maxRange, Integer.MAX_VALUE, filter, s, t, componentAccessor);
      }

      return count + this.npcs.countEntitiesInRange(minRange, maxRange, Integer.MAX_VALUE, filter, s, t, componentAccessor);
   }

   public void requirePlayerDistanceSorted(double v) {
      int value = MathUtil.ceil(v);
      if (!this.isConfiguring) {
         throw new IllegalStateException("function can be only called while configuring PositionCache");
      } else {
         this.players.requireDistanceSorted(value);
         RoleStats roleStats = this.role.getRoleStats();
         if (roleStats != null) {
            roleStats.trackRange(true, RoleStats.RangeType.SORTED, value);
         }
      }
   }

   public void requirePlayerDistanceUnsorted(double v) {
      int value = MathUtil.ceil(v);
      if (!this.isConfiguring) {
         throw new IllegalStateException("function can be only called while configuring PositionCache");
      } else {
         this.players.requireDistanceUnsorted(value);
         RoleStats roleStats = this.role.getRoleStats();
         if (roleStats != null) {
            roleStats.trackRange(true, RoleStats.RangeType.UNSORTED, value);
         }
      }
   }

   public void requirePlayerDistanceAvoidance(double v) {
      int value = MathUtil.ceil(v);
      if (!this.isConfiguring) {
         throw new IllegalStateException("function can be only called while configuring PositionCache");
      } else {
         this.players.requireDistanceAvoidance(value);
         RoleStats roleStats = this.role.getRoleStats();
         if (roleStats != null) {
            roleStats.trackRange(true, RoleStats.RangeType.AVOIDANCE, value);
         }
      }
   }

   public void requireEntityDistanceSorted(double v) {
      int value = MathUtil.ceil(v);
      if (!this.isConfiguring) {
         throw new IllegalStateException("function can be only called while configuring PositionCache");
      } else {
         this.npcs.requireDistanceSorted(value);
         RoleStats roleStats = this.role.getRoleStats();
         if (roleStats != null) {
            roleStats.trackRange(false, RoleStats.RangeType.SORTED, value);
         }
      }
   }

   public void requireEntityDistanceUnsorted(double v) {
      int value = MathUtil.ceil(v);
      if (!this.isConfiguring) {
         throw new IllegalStateException("function can be only called while configuring PositionCache");
      } else {
         this.npcs.requireDistanceUnsorted(value);
         RoleStats roleStats = this.role.getRoleStats();
         if (roleStats != null) {
            roleStats.trackRange(false, RoleStats.RangeType.UNSORTED, value);
         }
      }
   }

   public void requireEntityDistanceAvoidance(double v) {
      int value = MathUtil.ceil(v);
      if (!this.isConfiguring) {
         throw new IllegalStateException("function can be only called while configuring PositionCache");
      } else {
         value = this.npcs.requireDistanceAvoidance(value);
         RoleStats roleStats = this.role.getRoleStats();
         if (roleStats != null) {
            roleStats.trackRange(false, RoleStats.RangeType.AVOIDANCE, value);
         }
      }
   }

   public void requireDroppedItemDistance(double value) {
      if (this.maxDroppedItemDistance < value) {
         this.maxDroppedItemDistance = value;
      }
   }

   public void requireSpawnMarkerDistance(double value) {
      if (this.maxSpawnMarkerDistance < value) {
         this.maxSpawnMarkerDistance = value;
      }
   }

   public void requireSpawnBeaconDistance(int value) {
      if (this.maxSpawnBeaconDistance < value) {
         this.maxSpawnBeaconDistance = value;
      }
   }

   @Nonnull
   public Role getRole() {
      return this.role;
   }

   public <T, U, V, R> void forEachNPCUnordered(
      double maxDistance,
      @Nonnull QuadPredicate<Ref<EntityStore>, T, U, ComponentAccessor<EntityStore>> predicate,
      @Nonnull QuadConsumer<Ref<EntityStore>, T, V, R> consumer,
      T t,
      U u,
      V v,
      R r,
      ComponentAccessor<EntityStore> componentAccessor
   ) {
      this.npcs.forEachEntityUnordered(maxDistance, predicate, consumer, t, u, v, r, componentAccessor);
   }

   public <T> void forEachEntityInAvoidanceRange(
      @Nonnull Set<Ref<EntityStore>> ignoredEntitiesForAvoidance,
      @Nonnull TriConsumer<Ref<EntityStore>, T, CommandBuffer<EntityStore>> consumer,
      T t,
      CommandBuffer<EntityStore> commandBuffer
   ) {
      this.npcs.forEachEntityAvoidance(ignoredEntitiesForAvoidance, consumer, t, commandBuffer);
      this.players.forEachEntityAvoidance(ignoredEntitiesForAvoidance, consumer, t, commandBuffer);
   }

   public <T, U> void forEachEntityInAvoidanceRange(
      @Nonnull Set<Ref<EntityStore>> ignoredEntitiesForAvoidance,
      @Nonnull QuadConsumer<Ref<EntityStore>, T, U, CommandBuffer<EntityStore>> consumer,
      T t,
      U u,
      CommandBuffer<EntityStore> commandBuffer
   ) {
      this.npcs.forEachEntityAvoidance(ignoredEntitiesForAvoidance, consumer, t, u, commandBuffer);
      this.players.forEachEntityAvoidance(ignoredEntitiesForAvoidance, consumer, t, u, commandBuffer);
   }

   public void setOpaqueBlockSet(int blockSet) {
      this.opaqueBlockSet = blockSet;
   }

   private static <T> boolean testLineOfSightRays(
      @Nonnull Ref<EntityStore> ref,
      @Nonnull Ref<EntityStore> targetRef,
      @Nonnull PositionCache.RayPredicate<T> predicate,
      @Nonnull T t,
      @Nonnull ComponentAccessor<EntityStore> componentAccessor
   ) {
      TransformComponent transformComponent = componentAccessor.getComponent(ref, TRANSFORM_COMPONENT_TYPE);

      assert transformComponent != null;

      Vector3d position = transformComponent.getPosition();
      ModelComponent modelComponent = componentAccessor.getComponent(ref, MODEL_COMPONENT_TYPE);
      float eyeHeight = modelComponent != null ? modelComponent.getModel().getEyeHeight() : 0.0F;
      double sx = position.getX();
      double sy = position.getY() + eyeHeight;
      double sz = position.getZ();
      TransformComponent targetTransformComponent = componentAccessor.getComponent(targetRef, TRANSFORM_COMPONENT_TYPE);

      assert targetTransformComponent != null;

      Vector3d targetPosition = targetTransformComponent.getPosition();
      double tx = targetPosition.getX();
      double ty = targetPosition.getY();
      double tz = targetPosition.getZ();
      ModelComponent targetModelComponent = componentAccessor.getComponent(targetRef, MODEL_COMPONENT_TYPE);
      if (targetModelComponent != null) {
         return predicate.test(sx, sy, sz, tx, ty + targetModelComponent.getModel().getEyeHeight(), tz, t, componentAccessor);
      } else {
         double ox = 0.0;
         double oy = 0.0;
         double oz = 0.0;
         BoundingBox boundingBoxComponent = componentAccessor.getComponent(targetRef, BOUNDING_BOX_COMPONENT_TYPE);
         if (boundingBoxComponent != null) {
            Box boundingBox = boundingBoxComponent.getBoundingBox();
            ox = (boundingBox.getMax().getX() + boundingBox.getMin().getX()) / 2.0;
            oy = (boundingBox.getMax().getY() + boundingBox.getMin().getY()) / 2.0;
            oz = (boundingBox.getMax().getZ() + boundingBox.getMin().getZ()) / 2.0;
         }

         return predicate.test(sx, sy, sz, tx + ox, ty + oy, tz + oz, t, componentAccessor);
      }
   }

   private boolean hasLineOfSightInternal(
      @Nonnull Ref<EntityStore> ref, @Nonnull Ref<EntityStore> targetRef, @Nonnull ComponentAccessor<EntityStore> componentAccessor
   ) {
      if (ref.equals(targetRef)) {
         return false;
      } else {
         TransformComponent transformComponent = componentAccessor.getComponent(ref, TRANSFORM_COMPONENT_TYPE);

         assert transformComponent != null;

         TransformComponent targetTransformComponent = componentAccessor.getComponent(targetRef, TRANSFORM_COMPONENT_TYPE);

         assert targetTransformComponent != null;

         if (transformComponent.getPosition().distanceSquaredTo(targetTransformComponent.getPosition()) <= 1.0E-12) {
            return true;
         } else {
            World world = componentAccessor.getExternalData().getWorld();
            Objects.requireNonNull(world, "World can't be null in isLOS");
            Int2ObjectMap<IntSet> blockSets = BlockSetModule.getInstance().getBlockSets();
            IntSet opaqueSet = this.opaqueBlockSet >= 0 && blockSets != null ? blockSets.get(this.opaqueBlockSet) : null;

            boolean var9;
            try {
               this.lineOfSightComputeBuffer.result = true;
               this.lineOfSightComputeBuffer.assetMap = BlockType.getAssetMap();
               this.lineOfSightComputeBuffer.opaqueSet = opaqueSet;
               this.lineOfSightComputeBuffer.world = world;
               var9 = testLineOfSightRays(
                  ref,
                  targetRef,
                  (sx, sy, sz, tx, ty, tz, buffer, accessor) -> {
                     buffer.chunk = buffer.world.getChunkIfInMemory(ChunkUtil.indexChunkFromBlock(sx, sz));
                     if (buffer.chunk == null) {
                        return false;
                     } else {
                        BlockIterator.iterateFromTo(
                           sx,
                           sy,
                           sz,
                           tx,
                           ty,
                           tz,
                           (x, y, z, px, py, pz, qx, qy, qz, iBuffer) -> {
                              if (!ChunkUtil.isInsideChunk(iBuffer.chunk.getX(), iBuffer.chunk.getZ(), x, z)) {
                                 iBuffer.chunk = iBuffer.world.getChunkIfInMemory(ChunkUtil.indexChunkFromBlock(x, z));
                                 if (iBuffer.chunk == null) {
                                    iBuffer.result = false;
                                    return false;
                                 }
                              }

                              int blockId = iBuffer.chunk.getBlock(x, y, z);
                              if (blockId == 0) {
                                 return true;
                              } else {
                                 BlockType blockType = iBuffer.assetMap.getAsset(blockId);
                                 if (blockType != BlockType.UNKNOWN
                                    && blockType.getOpacity() != null
                                    && blockType.getOpacity() == Opacity.Transparent
                                    && (iBuffer.opaqueSet == null || !iBuffer.opaqueSet.contains(blockId))) {
                                    return true;
                                 } else {
                                    iBuffer.result = false;
                                    return false;
                                 }
                              }
                           },
                           buffer
                        );
                        return buffer.result;
                     }
                  },
                  this.lineOfSightComputeBuffer,
                  componentAccessor
               );
            } finally {
               this.lineOfSightComputeBuffer.clearRefs();
            }

            return var9;
         }
      }
   }

   public boolean hasLineOfSight(@Nonnull Ref<EntityStore> ref, @Nonnull Ref<EntityStore> targetRef, @Nonnull ComponentAccessor<EntityStore> componentAccessor) {
      boolean cached = this.lineOfSightCache.containsKey(targetRef);
      if (cached) {
         if (this.isBenchmarking) {
            NPCPlugin.get().collectSensorSupportLosTest(this.role.getRoleIndex(), true, 0L);
         }

         return this.lineOfSightCache.getByte(targetRef) != 0;
      } else {
         boolean hasLineOfSight;
         if (this.isBenchmarking) {
            long start = System.nanoTime();
            hasLineOfSight = this.hasLineOfSightInternal(ref, targetRef, componentAccessor);
            NPCPlugin.get().collectSensorSupportLosTest(this.role.getRoleIndex(), false, System.nanoTime() - start);
         } else {
            hasLineOfSight = this.hasLineOfSightInternal(ref, targetRef, componentAccessor);
         }

         this.lineOfSightCache.put(targetRef, (byte)(hasLineOfSight ? 1 : 0));
         return hasLineOfSight;
      }
   }

   public boolean hasInverseLineOfSight(
      @Nonnull Ref<EntityStore> ref, @Nonnull Ref<EntityStore> targetRef, @Nonnull ComponentAccessor<EntityStore> componentAccessor
   ) {
      boolean cached = this.inverseLineOfSightCache.containsKey(targetRef);
      if (this.isBenchmarking) {
         NPCPlugin.get().collectSensorSupportInverseLosTest(this.role.getRoleIndex(), cached);
      }

      if (cached) {
         return this.inverseLineOfSightCache.getByte(targetRef) != 0;
      } else {
         boolean hasLineOfSight = this.hasLineOfSightInternal(targetRef, ref, componentAccessor);
         this.inverseLineOfSightCache.put(targetRef, (byte)(hasLineOfSight ? 1 : 0));
         return hasLineOfSight;
      }
   }

   public boolean isFriendlyBlockingLineOfSight(
      @Nonnull Ref<EntityStore> ref, @Nonnull Ref<EntityStore> targetRef, @Nonnull ComponentAccessor<EntityStore> componentAccessor
   ) {
      boolean cached = this.friendlyFireCache.containsKey(targetRef);
      if (this.isBenchmarking) {
         NPCPlugin.get().collectSensorSupportFriendlyBlockingTest(this.role.getRoleIndex(), cached);
      }

      if (cached) {
         return this.friendlyFireCache.getByte(targetRef) != 0;
      } else {
         boolean blocking = testLineOfSightRays(
            ref,
            targetRef,
            (sx, sy, sz, tx, ty, tz, _this, accessor) -> {
               PositionCache.LineOfSightEntityBuffer buffer = _this.lineOfSightEntityComputeBuffer;
               buffer.pos.assign(sx, sy, sz);
               buffer.dir.assign(tx - sx, ty - sy, tz - sz);
               double squaredLength = buffer.dir.squaredLength();
               return squaredLength < 1.0E-6
                  ? false
                  : _this.players
                        .testAnyEntityDistanceSquared(
                           squaredLength,
                           (positionCache, targetRef1, buffer1, componentAccessor1, length2) -> positionCache.testLineOfSightEntity(
                              ref, targetRef1, buffer1, componentAccessor1, length2
                           ),
                           _this,
                           buffer,
                           accessor
                        )
                     || _this.npcs
                        .testAnyEntityDistanceSquared(
                           squaredLength,
                           (positionCache1, targetRef2, buffer2, componentAccessor2, length3) -> positionCache1.testLineOfSightEntity(
                              ref, targetRef2, buffer2, componentAccessor2, length3
                           ),
                           _this,
                           buffer,
                           accessor
                        );
            },
            this,
            componentAccessor
         );
         this.friendlyFireCache.put(targetRef, (byte)(blocking ? 1 : 0));
         return blocking;
      }
   }

   private boolean testLineOfSightEntity(
      @Nonnull Ref<EntityStore> ref,
      @Nonnull Ref<EntityStore> targetRef,
      @Nonnull PositionCache.LineOfSightEntityBuffer buffer,
      @Nonnull ComponentAccessor<EntityStore> componentAccessor,
      double length2
   ) {
      return !targetRef.equals(ref)
         && this.role.isFriendly(targetRef, componentAccessor)
         && rayIsIntersectingEntity(targetRef, buffer.pos, buffer.dir, buffer.minMax, length2, componentAccessor);
   }

   private void clearLineOfSightCache(double tickTime) {
      this.cacheTTL = (float)(this.cacheTTL - tickTime);
      if (this.cacheTTL <= 0.0F) {
         this.clearLineOfSightCache();
      }
   }

   private void clearLineOfSightCache() {
      this.cacheTTL = RandomExtra.randomRange(0.09F, 0.11F);
      this.lineOfSightCache.clear();
      this.inverseLineOfSightCache.clear();
      this.friendlyFireCache.clear();
   }

   protected static boolean rayIsIntersectingEntity(
      @Nonnull Ref<EntityStore> ref,
      @Nonnull Vector3d pos,
      @Nonnull Vector3d dir,
      @Nonnull Vector2d minMax,
      double length2,
      @Nonnull ComponentAccessor<EntityStore> componentAccessor
   ) {
      BoundingBox boundingBoxComponent = componentAccessor.getComponent(ref, BOUNDING_BOX_COMPONENT_TYPE);
      if (boundingBoxComponent == null) {
         return false;
      } else {
         TransformComponent transformComponent = componentAccessor.getComponent(ref, TRANSFORM_COMPONENT_TYPE);

         assert transformComponent != null;

         Vector3d position = transformComponent.getPosition();
         double px = position.getX();
         double py = position.getY();
         double pz = position.getZ();
         double dx = px - pos.x;
         double dy = py - pos.y;
         double dz = pz - pos.z;
         double dotProduct = NPCPhysicsMath.dotProduct(dir.x, dir.y, dir.z, dx, dy, dz);
         if (dotProduct <= 0.0) {
            return false;
         } else {
            double dist2 = NPCPhysicsMath.dotProduct(dx, dy, dz);
            return dotProduct * dotProduct >= dist2 * length2
               ? false
               : CollisionMath.intersectRayAABB(pos, dir, px, py, pz, boundingBoxComponent.getBoundingBox(), minMax);
         }
      }
   }

   @Nonnull
   public List<Ref<EntityStore>> getDroppedItemList() {
      return this.droppedItems;
   }

   @Nonnull
   public List<Ref<EntityStore>> getSpawnMarkerList() {
      return this.spawnMarkers;
   }

   @Nonnull
   public List<Ref<EntityStore>> getSpawnBeaconList() {
      return this.spawnBeacons;
   }

   private static class LineOfSightBuffer {
      @Nullable
      public World world;
      @Nullable
      public WorldChunk chunk;
      @Nullable
      public IntSet opaqueSet;
      @Nullable
      public BlockTypeAssetMap<String, BlockType> assetMap;
      public boolean result;

      public void clearRefs() {
         this.world = null;
         this.chunk = null;
         this.opaqueSet = null;
         this.assetMap = null;
      }
   }

   private static class LineOfSightEntityBuffer {
      public final Vector3d pos = new Vector3d();
      public final Vector3d dir = new Vector3d();
      public final Vector2d minMax = new Vector2d();
   }

   @FunctionalInterface
   public interface RayPredicate<T> {
      boolean test(double var1, double var3, double var5, double var7, double var9, double var11, T var13, @Nonnull ComponentAccessor<EntityStore> var14);
   }
}
