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

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

import it.unimi.dsi.fastutil.ints.Int2IntFunction;
import java.util.function.IntBinaryOperator;
import javax.annotation.Nullable;
import java.util.Arrays;
import com.hypixel.hytale.protocol.Opacity;
import com.hypixel.hytale.protocol.ColorLight;
import com.hypixel.hytale.server.core.universe.world.chunk.section.ChunkLightData;
import com.hypixel.hytale.server.core.asset.type.fluid.Fluid;
import it.unimi.dsi.fastutil.ints.IntIterator;
import it.unimi.dsi.fastutil.ints.IntSet;
import com.hypixel.hytale.math.vector.Vector2i;
import it.unimi.dsi.fastutil.ints.IntOpenHashSet;
import com.hypixel.hytale.server.core.universe.world.World;
import com.hypixel.hytale.math.util.MathUtil;
import com.hypixel.hytale.server.core.asset.type.blocktype.config.BlockType;
import java.util.BitSet;
import com.hypixel.hytale.server.core.universe.world.chunk.section.ChunkLightDataBuilder;
import com.hypixel.hytale.server.core.universe.world.storage.ChunkStore;
import com.hypixel.hytale.component.Ref;
import java.util.concurrent.atomic.AtomicLong;
import com.hypixel.hytale.server.core.universe.world.chunk.BlockChunk;
import com.hypixel.hytale.common.util.FormatUtil;
import com.hypixel.hytale.server.core.universe.world.chunk.section.FluidSection;
import java.util.concurrent.Executor;
import java.util.concurrent.CompletableFuture;
import java.util.Objects;
import com.hypixel.hytale.server.core.universe.world.accessor.ChunkAccessor;
import com.hypixel.hytale.server.core.universe.world.accessor.LocalCachedChunkAccessor;
import java.util.logging.Level;
import com.hypixel.hytale.math.util.ChunkUtil;
import javax.annotation.Nonnull;
import com.hypixel.hytale.server.core.universe.world.chunk.WorldChunk;
import com.hypixel.hytale.math.vector.Vector3i;
import com.hypixel.hytale.server.core.universe.world.chunk.section.BlockSection;
import com.hypixel.hytale.metrics.metric.AverageCollector;

public class FloodLightCalculation implements LightCalculation
{
    protected final ChunkLightingManager chunkLightingManager;
    protected final AverageCollector emptyAvg;
    protected final AverageCollector blocksAvg;
    protected final AverageCollector borderAvg;
    protected final AverageCollector avgChunk;
    protected final BlockSection[][] fromSections;
    
    public FloodLightCalculation(final ChunkLightingManager chunkLightingManager) {
        this.emptyAvg = new AverageCollector();
        this.blocksAvg = new AverageCollector();
        this.borderAvg = new AverageCollector();
        this.avgChunk = new AverageCollector();
        this.fromSections = new BlockSection[][] { new BlockSection[Vector3i.BLOCK_SIDES.length], new BlockSection[Vector3i.BLOCK_EDGES.length], new BlockSection[Vector3i.BLOCK_CORNERS.length] };
        this.chunkLightingManager = chunkLightingManager;
    }
    
    @Override
    public void init(@Nonnull final WorldChunk chunk) {
        this.chunkLightingManager.getWorld().debugAssertInTickingThread();
        final int x = chunk.getX();
        final int z = chunk.getZ();
        this.initChunk(chunk, x, z);
        this.initNeighbours(x, z);
    }
    
    private void initChunk(final int x, final int z) {
        final WorldChunk chunk = this.chunkLightingManager.getWorld().getChunkIfInMemory(ChunkUtil.indexChunk(x, z));
        if (chunk == null) {
            return;
        }
        this.initChunk(chunk, x, z);
    }
    
    private void initChunk(@Nonnull final WorldChunk chunk, final int x, final int z) {
        for (int y = 0; y < 10; ++y) {
            this.initSection(chunk, x, y, z);
        }
    }
    
    private void initNeighbours(final int x, final int z) {
        this.initChunk(x - 1, z - 1);
        this.initChunk(x - 1, z + 1);
        this.initChunk(x + 1, z - 1);
        this.initChunk(x + 1, z + 1);
        this.initChunk(x - 1, z);
        this.initChunk(x + 1, z);
        this.initChunk(x, z - 1);
        this.initChunk(x, z + 1);
    }
    
    private void initSection(@Nonnull final WorldChunk chunk, final int x, final int y, final int z) {
        final BlockSection section = chunk.getBlockChunk().getSectionAtIndex(y);
        if (!section.hasLocalLight()) {
            this.chunkLightingManager.getLogger().at(Level.FINEST).log("Init chunk %d, %d, %d because doesn't have local light", x, y, z);
        }
        else {
            if (section.hasGlobalLight()) {
                return;
            }
            this.chunkLightingManager.getLogger().at(Level.FINEST).log("Init chunk %d, %d, %d because doesn't have global light", x, y, z);
        }
        this.chunkLightingManager.addToQueue(new Vector3i(x, y, z));
    }
    
    private void initNeighbours(@Nonnull final LocalCachedChunkAccessor accessor, final int chunkX, final int chunkY, final int chunkZ) {
        this.initNeighbourSections(accessor, chunkX - 1, chunkY, chunkZ - 1);
        this.initNeighbourSections(accessor, chunkX - 1, chunkY, chunkZ + 1);
        this.initNeighbourSections(accessor, chunkX + 1, chunkY, chunkZ - 1);
        this.initNeighbourSections(accessor, chunkX + 1, chunkY, chunkZ + 1);
        this.initNeighbourSections(accessor, chunkX - 1, chunkY, chunkZ);
        this.initNeighbourSections(accessor, chunkX + 1, chunkY, chunkZ);
        this.initNeighbourSections(accessor, chunkX, chunkY, chunkZ - 1);
        this.initNeighbourSections(accessor, chunkX, chunkY, chunkZ + 1);
    }
    
    private void initNeighbourSections(@Nonnull final LocalCachedChunkAccessor accessor, final int x, final int y, final int z) {
        final WorldChunk chunk = accessor.getChunkIfInMemory(x, z);
        if (chunk == null) {
            return;
        }
        if (y < 9) {
            this.initSection(chunk, x, y + 1, z);
        }
        if (y > 0) {
            this.initSection(chunk, x, y - 1, z);
        }
    }
    
    @Nonnull
    @Override
    public CalculationResult calculateLight(@Nonnull final Vector3i chunkPosition) {
        final int chunkX = chunkPosition.x;
        final int chunkY = chunkPosition.y;
        final int chunkZ = chunkPosition.z;
        final WorldChunk worldChunk = this.chunkLightingManager.getWorld().getChunkIfInMemory(ChunkUtil.indexChunk(chunkX, chunkZ));
        if (worldChunk == null) {
            return CalculationResult.NOT_LOADED;
        }
        final AtomicLong chunkLightTiming = worldChunk.chunkLightTiming;
        final boolean fineLoggable = this.chunkLightingManager.getLogger().at(Level.FINE).isEnabled();
        final LocalCachedChunkAccessor accessor = LocalCachedChunkAccessor.atChunkCoords(this.chunkLightingManager.getWorld(), chunkX, chunkZ, 1);
        accessor.overwrite(worldChunk);
        final LocalCachedChunkAccessor obj = accessor;
        Objects.requireNonNull(obj);
        CompletableFuture.runAsync(obj::cacheChunksInRadius, this.chunkLightingManager.getWorld()).join();
        final BlockSection toSection = worldChunk.getBlockChunk().getSectionAtIndex(chunkY);
        final FluidSection fluidSection = CompletableFuture.supplyAsync(() -> {
            final Ref<ChunkStore> section2 = this.chunkLightingManager.getWorld().getChunkStore().getChunkSectionReference(chunkX, chunkY, chunkZ);
            if (section2 == null) {
                return null;
            }
            else {
                return (FluidSection)section2.getStore().getComponent(section2, FluidSection.getComponentType());
            }
        }, this.chunkLightingManager.getWorld()).join();
        if (fluidSection == null) {
            return CalculationResult.NOT_LOADED;
        }
        if (toSection.hasLocalLight() && toSection.hasGlobalLight()) {
            this.initNeighbours(accessor, chunkX, chunkY, chunkZ);
            return CalculationResult.DONE;
        }
        if (!toSection.hasLocalLight()) {
            final CalculationResult localLightResult = this.updateLocalLight(accessor, worldChunk, chunkX, chunkY, chunkZ, toSection, fluidSection, chunkLightTiming, fineLoggable);
            switch (localLightResult) {
                case NOT_LOADED:
                case INVALIDATED:
                case WAITING_FOR_NEIGHBOUR: {
                    return localLightResult;
                }
                default: {
                    this.initNeighbours(accessor, chunkX, chunkY, chunkZ);
                    break;
                }
            }
        }
        if (!toSection.hasGlobalLight()) {
            final CalculationResult globalLightResult = this.updateGlobalLight(accessor, worldChunk, chunkX, chunkY, chunkZ, toSection, chunkLightTiming, fineLoggable);
            switch (globalLightResult) {
                case NOT_LOADED:
                case INVALIDATED:
                case WAITING_FOR_NEIGHBOUR: {
                    return globalLightResult;
                }
            }
        }
        if (fineLoggable) {
            final long chunkDiff = chunkLightTiming.get();
            boolean done = chunkDiff != 0L;
            for (int i = 0; i < 10; ++i) {
                final BlockSection section = worldChunk.getBlockChunk().getSectionAtIndex(i);
                done = (done && section.hasLocalLight() && section.hasGlobalLight());
            }
            if (done) {
                this.avgChunk.add((double)chunkDiff);
                this.chunkLightingManager.getLogger().at(Level.FINE).log("Flood Chunk: Took %s at %d, %d - Avg: %s", FormatUtil.nanosToString(chunkDiff), chunkX, chunkZ, FormatUtil.nanosToString((long)this.avgChunk.get()));
            }
        }
        if (BlockChunk.SEND_LOCAL_LIGHTING_DATA || BlockChunk.SEND_GLOBAL_LIGHTING_DATA) {
            worldChunk.getBlockChunk().invalidateChunkSection(chunkY);
        }
        return CalculationResult.DONE;
    }
    
    @Nonnull
    public CalculationResult updateLocalLight(final LocalCachedChunkAccessor accessor, @Nonnull final WorldChunk worldChunk, final int chunkX, final int chunkY, final int chunkZ, @Nonnull final BlockSection toSection, @Nonnull final FluidSection fluidSection, @Nonnull final AtomicLong chunkLightTiming, final boolean fineLoggable) {
        final long start = System.nanoTime();
        final boolean solidAir = toSection.isSolidAir() && fluidSection.isEmpty();
        ChunkLightDataBuilder localLight;
        if (solidAir) {
            localLight = this.floodEmptyChunkSection(worldChunk, toSection.getLocalChangeCounter(), chunkY);
        }
        else {
            localLight = this.floodChunkSection(worldChunk, toSection, fluidSection, chunkY);
        }
        toSection.setLocalLight(localLight);
        worldChunk.markNeedsSaving();
        if (fineLoggable) {
            final long end = System.nanoTime();
            final long diff = end - start;
            if (solidAir) {
                this.emptyAvg.add((double)diff);
            }
            else {
                this.blocksAvg.add((double)diff);
            }
            chunkLightTiming.addAndGet(diff);
            this.chunkLightingManager.getLogger().at(Level.FINER).log("Flood Chunk Section (local): Took %s at %d, %d, %d - %s Avg: %s", FormatUtil.nanosToString(diff), chunkX, chunkY, chunkZ, solidAir ? "air" : "blocks", FormatUtil.nanosToString((long)(solidAir ? this.emptyAvg.get() : this.blocksAvg.get())));
        }
        if (!toSection.hasLocalLight()) {
            this.chunkLightingManager.getLogger().at(Level.FINEST).log("Chunk Section still needs relight! (local) %d, %d, %d - %d != %d (counter != id)", chunkX, chunkY, chunkZ, toSection.getLocalChangeCounter(), toSection.getLocalLight().getChangeId());
            return CalculationResult.INVALIDATED;
        }
        return CalculationResult.DONE;
    }
    
    @Nonnull
    public CalculationResult updateGlobalLight(@Nonnull final LocalCachedChunkAccessor accessor, @Nonnull final WorldChunk worldChunk, final int chunkX, final int chunkY, final int chunkZ, @Nonnull final BlockSection toSection, @Nonnull final AtomicLong chunkLightTiming, final boolean fineLoggable) {
        final long start = System.nanoTime();
        if (this.testNeighboursForLocalLight(accessor, worldChunk, chunkX, chunkY, chunkZ)) {
            return CalculationResult.WAITING_FOR_NEIGHBOUR;
        }
        final ChunkLightDataBuilder globalLight = new ChunkLightDataBuilder(toSection.getLocalLight(), toSection.getGlobalChangeCounter());
        final BitSet bitSetQueue = new BitSet(32768);
        this.propagateSides(toSection, globalLight, bitSetQueue);
        this.propagateEdges(toSection, globalLight, bitSetQueue);
        this.propagateCorners(toSection, globalLight, bitSetQueue);
        this.propagateLight(bitSetQueue, toSection, globalLight);
        toSection.setGlobalLight(globalLight);
        worldChunk.markNeedsSaving();
        if (fineLoggable) {
            final long end = System.nanoTime();
            final long diff = end - start;
            chunkLightTiming.addAndGet(diff);
            this.borderAvg.add((double)diff);
            this.chunkLightingManager.getLogger().at(Level.FINER).log("Flood Chunk Section (global): Took " + FormatUtil.nanosToString(diff) + " at " + chunkX + ", " + chunkY + ", " + chunkZ + " - Avg: " + FormatUtil.nanosToString((long)this.borderAvg.get()));
        }
        if (!toSection.hasGlobalLight()) {
            this.chunkLightingManager.getLogger().at(Level.FINEST).log("Chunk Section still needs relight! (global) %d, %d, %d - %d != %d (counter != id)", chunkX, chunkY, chunkZ, toSection.getGlobalChangeCounter(), toSection.getGlobalLight().getChangeId());
            return CalculationResult.INVALIDATED;
        }
        return CalculationResult.DONE;
    }
    
    @Override
    public boolean invalidateLightAtBlock(@Nonnull final WorldChunk worldChunk, final int blockX, final int blockY, final int blockZ, @Nonnull final BlockType blockType, final int oldHeight, final int newHeight) {
        final int chunkX = worldChunk.getX();
        final int chunkY = blockY >> 5;
        final int chunkZ = worldChunk.getZ();
        final int oldHeightChunk = oldHeight >> 5;
        final int newHeightChunk = newHeight >> 5;
        final int from = Math.max(MathUtil.minValue(oldHeightChunk, newHeightChunk, chunkY), 0);
        final int to = MathUtil.maxValue(oldHeightChunk, newHeightChunk, chunkY) + 1;
        final boolean handled = this.invalidateLightInChunkSections(worldChunk, from, to);
        this.chunkLightingManager.getLogger().at(Level.FINER).log("updateLightAtBlock(%d, %d, %d, %s): %d, %d, %d", blockX, blockY, blockZ, blockType.getId(), chunkX, chunkY, chunkZ);
        return handled;
    }
    
    @Override
    public boolean invalidateLightInChunkSections(@Nonnull final WorldChunk worldChunk, final int sectionIndexFrom, final int sectionIndexTo) {
        final int chunkX = worldChunk.getX();
        final int chunkZ = worldChunk.getZ();
        final World world = this.chunkLightingManager.getWorld();
        final LocalCachedChunkAccessor accessor = LocalCachedChunkAccessor.atChunkCoords(world, chunkX, chunkZ, 1);
        accessor.overwrite(worldChunk);
        if (!world.isInThread()) {
            final LocalCachedChunkAccessor obj = accessor;
            Objects.requireNonNull(obj);
            CompletableFuture.runAsync(obj::cacheChunksInRadius, world).join();
        }
        else {
            accessor.cacheChunksInRadius();
        }
        for (int x = chunkX - 1; x <= chunkX + 1; ++x) {
            for (int z = chunkZ - 1; z <= chunkZ + 1; ++z) {
                final WorldChunk worldChunkTemp = accessor.getChunkIfInMemory(x, z);
                if (worldChunkTemp != null) {
                    for (int y = sectionIndexTo - 1; y >= sectionIndexFrom; --y) {
                        final BlockSection section = worldChunkTemp.getBlockChunk().getSectionAtIndex(y);
                        if (worldChunkTemp == worldChunk) {
                            section.invalidateLocalLight();
                        }
                        else {
                            section.invalidateGlobalLight();
                        }
                        if (BlockChunk.SEND_LOCAL_LIGHTING_DATA || BlockChunk.SEND_GLOBAL_LIGHTING_DATA) {
                            worldChunkTemp.getBlockChunk().invalidateChunkSection(y);
                        }
                    }
                }
            }
        }
        for (int x = chunkX - 1; x <= chunkX + 1; ++x) {
            for (int z = chunkZ - 1; z <= chunkZ + 1; ++z) {
                final WorldChunk worldChunkTemp = accessor.getChunkIfInMemory(x, z);
                if (worldChunkTemp != null) {
                    for (int y = sectionIndexTo - 1; y >= sectionIndexFrom; --y) {
                        this.chunkLightingManager.addToQueue(new Vector3i(x, y, z));
                    }
                }
            }
        }
        return false;
    }
    
    @Nonnull
    private ChunkLightDataBuilder floodEmptyChunkSection(@Nonnull final WorldChunk worldChunk, final short changeCounter, final int chunkY) {
        final int sectionY = chunkY * 32;
        final ChunkLightDataBuilder light = new ChunkLightDataBuilder(changeCounter);
        final BitSet bitSetQueue = new BitSet(1024);
        for (int x = 0; x < 32; ++x) {
            for (int z = 0; z < 32; ++z) {
                final int column = ChunkUtil.indexColumn(x, z);
                final short height = worldChunk.getHeight(column);
                if (sectionY > height) {
                    for (int y = 0; y < 32; ++y) {
                        light.setLight(ChunkUtil.indexBlockFromColumn(column, y), 3, (byte)15);
                    }
                    bitSetQueue.set(column);
                }
            }
        }
        if (bitSetQueue.cardinality() < 1024) {
            final IntSet changedColumns = new IntOpenHashSet(1024);
            int counter = 0;
            while (true) {
                final int column = bitSetQueue.nextSetBit(counter);
                if (column == -1) {
                    if (bitSetQueue.isEmpty()) {
                        break;
                    }
                    counter = 0;
                }
                else {
                    bitSetQueue.clear(column);
                    counter = column;
                    final int x2 = ChunkUtil.xFromColumn(column);
                    final int z2 = ChunkUtil.zFromColumn(column);
                    final byte skyLight = light.getLight(column, 3);
                    final byte propagatedValue = (byte)(skyLight - 1);
                    if (propagatedValue < 1) {
                        continue;
                    }
                    for (final Vector2i side : Vector2i.DIRECTIONS) {
                        final int nx = x2 + side.x;
                        final int nz = z2 + side.y;
                        if (nx >= 0) {
                            if (nx < 32) {
                                if (nz >= 0) {
                                    if (nz < 32) {
                                        final int neighbourColumn = ChunkUtil.indexColumn(nx, nz);
                                        final byte neighbourSkyLight = light.getLight(neighbourColumn, 3);
                                        if (neighbourSkyLight < propagatedValue) {
                                            light.setLight(neighbourColumn, 3, propagatedValue);
                                            changedColumns.add(neighbourColumn);
                                            if (propagatedValue > 1) {
                                                bitSetQueue.set(neighbourColumn);
                                            }
                                        }
                                    }
                                }
                            }
                        }
                    }
                }
            }
            final IntIterator iterator = changedColumns.iterator();
            while (iterator.hasNext()) {
                final int column2 = iterator.nextInt();
                final byte skyLight2 = light.getLight(column2, 3);
                for (int y2 = 1; y2 < 32; ++y2) {
                    light.setLight(ChunkUtil.indexBlockFromColumn(column2, y2), 3, skyLight2);
                }
            }
        }
        return light;
    }
    
    @Nonnull
    private ChunkLightDataBuilder floodChunkSection(@Nonnull final WorldChunk worldChunk, @Nonnull final BlockSection toSection, @Nonnull final FluidSection fluidSection, final int chunkY) {
        final int sectionY = chunkY * 32;
        final ChunkLightDataBuilder toLight = new ChunkLightDataBuilder(toSection.getLocalChangeCounter());
        final BitSet bitSetQueue = new BitSet(32768);
        for (int x = 0; x < 32; ++x) {
            for (int z = 0; z < 32; ++z) {
                final int column = ChunkUtil.indexColumn(x, z);
                final short height = worldChunk.getHeight(column);
                for (int y = 0; y < 32; ++y) {
                    final int blockIndex = ChunkUtil.indexBlockFromColumn(column, y);
                    final byte skyValue = this.getSkyValue(worldChunk, chunkY, x, y, z, sectionY, height);
                    short lightValue = (short)(skyValue << 12);
                    final int blockId = toSection.get(blockIndex);
                    final BlockType blockType = BlockType.getAssetMap().getAsset(blockId);
                    final ColorLight blockTypeLight = blockType.getLight();
                    final int fluidId = fluidSection.getFluidId(blockIndex);
                    final Fluid fluid = Fluid.getAssetMap().getAsset(fluidId);
                    final ColorLight fluidLight = fluid.getLight();
                    if (blockTypeLight != null && fluidLight != null) {
                        lightValue = ChunkLightData.combineLightValues((byte)Math.max(blockTypeLight.red, fluidLight.red), (byte)Math.max(blockTypeLight.green, fluidLight.green), (byte)Math.max(blockTypeLight.blue, fluidLight.blue), skyValue);
                    }
                    else if (fluidLight != null) {
                        lightValue = ChunkLightData.combineLightValues(fluidLight.red, fluidLight.green, fluidLight.blue, skyValue);
                    }
                    else if (blockTypeLight != null) {
                        lightValue = ChunkLightData.combineLightValues(blockTypeLight.red, blockTypeLight.green, blockTypeLight.blue, skyValue);
                    }
                    if (lightValue != 0) {
                        toLight.setLightRaw(blockIndex, lightValue);
                        bitSetQueue.set(blockIndex);
                    }
                }
            }
        }
        this.propagateLight(bitSetQueue, toSection, toLight);
        return toLight;
    }
    
    protected byte getSkyValue(final WorldChunk worldChunk, final int chunkY, final int blockX, final int blockY, final int blockZ, final int sectionY, final int height) {
        final int originY = sectionY + blockY;
        final boolean hasSky = originY >= height;
        return (byte)(hasSky ? 15 : 0);
    }
    
    private void propagateLight(@Nonnull final BitSet bitSetQueue, @Nonnull final BlockSection section, @Nonnull final ChunkLightDataBuilder light) {
        int counter = 0;
        while (true) {
            final int blockIndex = bitSetQueue.nextSetBit(counter);
            if (blockIndex == -1) {
                if (bitSetQueue.isEmpty()) {
                    break;
                }
                counter = 0;
            }
            else {
                bitSetQueue.clear(blockIndex);
                counter = blockIndex;
                final BlockType fromBlockType = BlockType.getAssetMap().getAsset(section.get(blockIndex));
                final Opacity fromOpacity = fromBlockType.getOpacity();
                if (fromOpacity == Opacity.Solid) {
                    continue;
                }
                final short lightValue = light.getLightRaw(blockIndex);
                final byte redLight = ChunkLightData.getLightValue(lightValue, 0);
                final byte greenLight = ChunkLightData.getLightValue(lightValue, 1);
                final byte blueLight = ChunkLightData.getLightValue(lightValue, 2);
                final byte skyLight = ChunkLightData.getLightValue(lightValue, 3);
                if (redLight < 2 && greenLight < 2 && blueLight < 2 && skyLight < 2) {
                    continue;
                }
                byte propagatedRedValue = (byte)(redLight - 1);
                byte propagatedGreenValue = (byte)(greenLight - 1);
                byte propagatedBlueValue = (byte)(blueLight - 1);
                byte propagatedSkyValue = (byte)(skyLight - 1);
                if (fromOpacity == Opacity.Semitransparent || fromOpacity == Opacity.Cutout) {
                    --propagatedRedValue;
                    --propagatedGreenValue;
                    --propagatedBlueValue;
                    --propagatedSkyValue;
                }
                if (propagatedRedValue < 1 && propagatedGreenValue < 1 && propagatedBlueValue < 1 && propagatedSkyValue < 1) {
                    continue;
                }
                final int x = ChunkUtil.xFromIndex(blockIndex);
                final int y = ChunkUtil.yFromIndex(blockIndex);
                final int z = ChunkUtil.zFromIndex(blockIndex);
                for (final Vector3i side : Vector3i.BLOCK_SIDES) {
                    final int nx = x + side.x;
                    if (nx >= 0) {
                        if (nx < 32) {
                            final int ny = y + side.y;
                            if (ny >= 0) {
                                if (ny < 32) {
                                    final int nz = z + side.z;
                                    if (nz >= 0) {
                                        if (nz < 32) {
                                            final int neighbourBlock = ChunkUtil.indexBlock(nx, ny, nz);
                                            this.propagateLight(bitSetQueue, propagatedRedValue, propagatedGreenValue, propagatedBlueValue, propagatedSkyValue, section, light, neighbourBlock);
                                        }
                                    }
                                }
                            }
                        }
                    }
                }
            }
        }
    }
    
    public boolean testNeighboursForLocalLight(@Nonnull final LocalCachedChunkAccessor accessor, @Nonnull final WorldChunk worldChunk, final int chunkX, final int chunkY, final int chunkZ) {
        final Vector3i[][] blockParts = Vector3i.BLOCK_PARTS;
        for (int partType = 0; partType < this.fromSections.length; ++partType) {
            final BlockSection[] partSections = this.fromSections[partType];
            Arrays.fill(partSections, null);
            final Vector3i[] directions = blockParts[partType];
            for (int i = 0; i < directions.length; ++i) {
                final Vector3i side = directions[i];
                final int nx = chunkX + side.x;
                final int ny = chunkY + side.y;
                final int nz = chunkZ + side.z;
                if (ny >= 0) {
                    if (ny < 10) {
                        if (nx == chunkX && nz == chunkZ) {
                            final BlockSection fromSection = worldChunk.getBlockChunk().getSectionAtIndex(ny);
                            if (!fromSection.hasLocalLight()) {
                                return true;
                            }
                            partSections[i] = fromSection;
                        }
                        else {
                            final WorldChunk neighbourChunk = accessor.getChunkIfInMemory(nx, nz);
                            if (neighbourChunk == null) {
                                return true;
                            }
                            final BlockSection fromSection2 = neighbourChunk.getBlockChunk().getSectionAtIndex(ny);
                            if (!fromSection2.hasLocalLight()) {
                                return true;
                            }
                            partSections[i] = fromSection2;
                        }
                    }
                }
            }
        }
        return false;
    }
    
    public void propagateSides(@Nonnull final BlockSection toSection, @Nonnull final ChunkLightDataBuilder globalLight, @Nonnull final BitSet bitSetQueue) {
        final BlockSection[] fromSectionsSides = this.fromSections[0];
        int i = 0;
        this.propagateSide(bitSetQueue, fromSectionsSides[i++], toSection, globalLight, (a, b) -> ChunkUtil.indexBlock(a, 0, b), (a, b) -> ChunkUtil.indexBlock(a, 31, b));
        this.propagateSide(bitSetQueue, fromSectionsSides[i++], toSection, globalLight, (a, b) -> ChunkUtil.indexBlock(a, 31, b), (a, b) -> ChunkUtil.indexBlock(a, 0, b));
        this.propagateSide(bitSetQueue, fromSectionsSides[i++], toSection, globalLight, (a, b) -> ChunkUtil.indexBlock(a, b, 31), (a, b) -> ChunkUtil.indexBlock(a, b, 0));
        this.propagateSide(bitSetQueue, fromSectionsSides[i++], toSection, globalLight, (a, b) -> ChunkUtil.indexBlock(a, b, 0), (a, b) -> ChunkUtil.indexBlock(a, b, 31));
        this.propagateSide(bitSetQueue, fromSectionsSides[i++], toSection, globalLight, (a, b) -> ChunkUtil.indexBlock(31, a, b), (a, b) -> ChunkUtil.indexBlock(0, a, b));
        this.propagateSide(bitSetQueue, fromSectionsSides[i++], toSection, globalLight, (a, b) -> ChunkUtil.indexBlock(0, a, b), (a, b) -> ChunkUtil.indexBlock(31, a, b));
    }
    
    private void propagateSide(@Nonnull final BitSet bitSetQueue, @Nullable final BlockSection fromSection, @Nonnull final BlockSection toSection, @Nonnull final ChunkLightDataBuilder toLight, @Nonnull final IntBinaryOperator fromIndex, @Nonnull final IntBinaryOperator toIndex) {
        if (fromSection == null) {
            return;
        }
        final ChunkLightData fromLight = fromSection.getLocalLight();
        for (int a = 0; a < 32; ++a) {
            for (int b = 0; b < 32; ++b) {
                final int fromBlockIndex = fromIndex.applyAsInt(a, b);
                final int toBlockIndex = toIndex.applyAsInt(a, b);
                final BlockType fromBlockType = BlockType.getAssetMap().getAsset(fromSection.get(fromBlockIndex));
                final Opacity fromOpacity = fromBlockType.getOpacity();
                if (fromOpacity != Opacity.Solid) {
                    final short lightValue = fromLight.getLightRaw(fromBlockIndex);
                    final byte redLight = ChunkLightData.getLightValue(lightValue, 0);
                    final byte greenLight = ChunkLightData.getLightValue(lightValue, 1);
                    final byte blueLight = ChunkLightData.getLightValue(lightValue, 2);
                    final byte skyLight = ChunkLightData.getLightValue(lightValue, 3);
                    if (redLight >= 2 || greenLight >= 2 || blueLight >= 2 || skyLight >= 2) {
                        byte propagatedRedValue = (byte)(redLight - 1);
                        byte propagatedGreenValue = (byte)(greenLight - 1);
                        byte propagatedBlueValue = (byte)(blueLight - 1);
                        byte propagatedSkyValue = (byte)(skyLight - 1);
                        if (fromOpacity == Opacity.Semitransparent || fromOpacity == Opacity.Cutout) {
                            --propagatedRedValue;
                            --propagatedGreenValue;
                            --propagatedBlueValue;
                            --propagatedSkyValue;
                        }
                        if (propagatedRedValue >= 1 || propagatedGreenValue >= 1 || propagatedBlueValue >= 1 || propagatedSkyValue >= 1) {
                            this.propagateLight(bitSetQueue, propagatedRedValue, propagatedGreenValue, propagatedBlueValue, propagatedSkyValue, toSection, toLight, toBlockIndex);
                        }
                    }
                }
            }
        }
    }
    
    public void propagateEdges(@Nonnull final BlockSection toSection, @Nonnull final ChunkLightDataBuilder globalLight, @Nonnull final BitSet bitSetQueue) {
        final BlockSection[] fromSectionsEdges = this.fromSections[1];
        int i = 0;
        this.propagateEdge(bitSetQueue, fromSectionsEdges[i++], toSection, globalLight, a -> ChunkUtil.indexBlock(a, 0, 31), a -> ChunkUtil.indexBlock(a, 31, 0));
        this.propagateEdge(bitSetQueue, fromSectionsEdges[i++], toSection, globalLight, a -> ChunkUtil.indexBlock(a, 31, 31), a -> ChunkUtil.indexBlock(a, 0, 0));
        this.propagateEdge(bitSetQueue, fromSectionsEdges[i++], toSection, globalLight, a -> ChunkUtil.indexBlock(a, 0, 0), a -> ChunkUtil.indexBlock(a, 31, 31));
        this.propagateEdge(bitSetQueue, fromSectionsEdges[i++], toSection, globalLight, a -> ChunkUtil.indexBlock(a, 31, 0), a -> ChunkUtil.indexBlock(a, 0, 31));
        this.propagateEdge(bitSetQueue, fromSectionsEdges[i++], toSection, globalLight, a -> ChunkUtil.indexBlock(31, 0, a), a -> ChunkUtil.indexBlock(0, 31, a));
        this.propagateEdge(bitSetQueue, fromSectionsEdges[i++], toSection, globalLight, a -> ChunkUtil.indexBlock(31, 31, a), a -> ChunkUtil.indexBlock(0, 0, a));
        this.propagateEdge(bitSetQueue, fromSectionsEdges[i++], toSection, globalLight, a -> ChunkUtil.indexBlock(0, 0, a), a -> ChunkUtil.indexBlock(31, 31, a));
        this.propagateEdge(bitSetQueue, fromSectionsEdges[i++], toSection, globalLight, a -> ChunkUtil.indexBlock(0, 31, a), a -> ChunkUtil.indexBlock(31, 0, a));
        this.propagateEdge(bitSetQueue, fromSectionsEdges[i++], toSection, globalLight, a -> ChunkUtil.indexBlock(31, a, 31), a -> ChunkUtil.indexBlock(0, a, 0));
        this.propagateEdge(bitSetQueue, fromSectionsEdges[i++], toSection, globalLight, a -> ChunkUtil.indexBlock(0, a, 31), a -> ChunkUtil.indexBlock(31, a, 0));
        this.propagateEdge(bitSetQueue, fromSectionsEdges[i++], toSection, globalLight, a -> ChunkUtil.indexBlock(31, a, 0), a -> ChunkUtil.indexBlock(0, a, 31));
        this.propagateEdge(bitSetQueue, fromSectionsEdges[i++], toSection, globalLight, a -> ChunkUtil.indexBlock(0, a, 0), a -> ChunkUtil.indexBlock(31, a, 31));
    }
    
    private void propagateEdge(@Nonnull final BitSet bitSetQueue, @Nullable final BlockSection fromSection, @Nonnull final BlockSection toSection, @Nonnull final ChunkLightDataBuilder toLight, @Nonnull final Int2IntFunction fromIndex, @Nonnull final Int2IntFunction toIndex) {
        if (fromSection == null) {
            return;
        }
        final ChunkLightData fromLight = fromSection.getLocalLight();
        for (int a = 0; a < 32; ++a) {
            final int fromBlockIndex = fromIndex.applyAsInt(a);
            final int toBlockIndex = toIndex.applyAsInt(a);
            final BlockType fromBlockType = BlockType.getAssetMap().getAsset(fromSection.get(fromBlockIndex));
            final Opacity fromOpacity = fromBlockType.getOpacity();
            if (fromOpacity != Opacity.Solid) {
                final short lightValue = fromLight.getLightRaw(fromBlockIndex);
                final byte redLight = ChunkLightData.getLightValue(lightValue, 0);
                final byte greenLight = ChunkLightData.getLightValue(lightValue, 1);
                final byte blueLight = ChunkLightData.getLightValue(lightValue, 2);
                final byte skyLight = ChunkLightData.getLightValue(lightValue, 3);
                if (redLight >= 3 || greenLight >= 3 || blueLight >= 3 || skyLight >= 3) {
                    byte propagatedRedValue = (byte)(redLight - 2);
                    byte propagatedGreenValue = (byte)(greenLight - 2);
                    byte propagatedBlueValue = (byte)(blueLight - 2);
                    byte propagatedSkyValue = (byte)(skyLight - 2);
                    if (fromOpacity == Opacity.Semitransparent || fromOpacity == Opacity.Cutout) {
                        --propagatedRedValue;
                        --propagatedGreenValue;
                        --propagatedBlueValue;
                        --propagatedSkyValue;
                    }
                    if (propagatedRedValue >= 1 || propagatedGreenValue >= 1 || propagatedBlueValue >= 1 || propagatedSkyValue >= 1) {
                        this.propagateLight(bitSetQueue, propagatedRedValue, propagatedGreenValue, propagatedBlueValue, propagatedSkyValue, toSection, toLight, toBlockIndex);
                    }
                }
            }
        }
    }
    
    public void propagateCorners(@Nonnull final BlockSection toSection, @Nonnull final ChunkLightDataBuilder globalLight, @Nonnull final BitSet bitSetQueue) {
        final BlockSection[] fromSectionsCorners = this.fromSections[2];
        int i = 0;
        this.propagateCorner(bitSetQueue, fromSectionsCorners[i++], toSection, globalLight, ChunkUtil.indexBlock(31, 0, 31), ChunkUtil.indexBlock(0, 31, 0));
        this.propagateCorner(bitSetQueue, fromSectionsCorners[i++], toSection, globalLight, ChunkUtil.indexBlock(0, 0, 31), ChunkUtil.indexBlock(31, 31, 0));
        this.propagateCorner(bitSetQueue, fromSectionsCorners[i++], toSection, globalLight, ChunkUtil.indexBlock(31, 31, 31), ChunkUtil.indexBlock(0, 0, 0));
        this.propagateCorner(bitSetQueue, fromSectionsCorners[i++], toSection, globalLight, ChunkUtil.indexBlock(0, 31, 31), ChunkUtil.indexBlock(31, 0, 0));
        this.propagateCorner(bitSetQueue, fromSectionsCorners[i++], toSection, globalLight, ChunkUtil.indexBlock(31, 0, 0), ChunkUtil.indexBlock(0, 31, 31));
        this.propagateCorner(bitSetQueue, fromSectionsCorners[i++], toSection, globalLight, ChunkUtil.indexBlock(0, 0, 0), ChunkUtil.indexBlock(31, 31, 31));
        this.propagateCorner(bitSetQueue, fromSectionsCorners[i++], toSection, globalLight, ChunkUtil.indexBlock(31, 31, 0), ChunkUtil.indexBlock(0, 0, 31));
        this.propagateCorner(bitSetQueue, fromSectionsCorners[i++], toSection, globalLight, ChunkUtil.indexBlock(0, 31, 0), ChunkUtil.indexBlock(31, 0, 31));
    }
    
    private void propagateCorner(@Nonnull final BitSet bitSetQueue, @Nullable final BlockSection fromSection, @Nonnull final BlockSection toSection, @Nonnull final ChunkLightDataBuilder toLight, final int fromBlockIndex, final int toBlockIndex) {
        if (fromSection == null) {
            return;
        }
        final ChunkLightData fromLight = fromSection.getLocalLight();
        final BlockType fromBlockType = BlockType.getAssetMap().getAsset(fromSection.get(fromBlockIndex));
        final Opacity fromOpacity = fromBlockType.getOpacity();
        if (fromOpacity == Opacity.Solid) {
            return;
        }
        final short lightValue = fromLight.getLightRaw(fromBlockIndex);
        final byte redLight = ChunkLightData.getLightValue(lightValue, 0);
        final byte greenLight = ChunkLightData.getLightValue(lightValue, 1);
        final byte blueLight = ChunkLightData.getLightValue(lightValue, 2);
        final byte skyLight = ChunkLightData.getLightValue(lightValue, 3);
        if (redLight < 4 && greenLight < 4 && blueLight < 4 && skyLight < 4) {
            return;
        }
        byte propagatedRedValue = (byte)(redLight - 3);
        byte propagatedGreenValue = (byte)(greenLight - 3);
        byte propagatedBlueValue = (byte)(blueLight - 3);
        byte propagatedSkyValue = (byte)(skyLight - 3);
        if (fromOpacity == Opacity.Semitransparent || fromOpacity == Opacity.Cutout) {
            --propagatedRedValue;
            --propagatedGreenValue;
            --propagatedBlueValue;
            --propagatedSkyValue;
        }
        if (propagatedRedValue < 1 && propagatedGreenValue < 1 && propagatedBlueValue < 1 && propagatedSkyValue < 1) {
            return;
        }
        this.propagateLight(bitSetQueue, propagatedRedValue, propagatedGreenValue, propagatedBlueValue, propagatedSkyValue, toSection, toLight, toBlockIndex);
    }
    
    private void propagateLight(@Nonnull final BitSet bitSetQueue, byte propagatedRedValue, byte propagatedGreenValue, byte propagatedBlueValue, byte propagatedSkyValue, @Nonnull final BlockSection toSection, @Nonnull final ChunkLightDataBuilder toLight, final int toBlockIndex) {
        final BlockType toBlockType = BlockType.getAssetMap().getAsset(toSection.get(toBlockIndex));
        final Opacity toOpacity = toBlockType.getOpacity();
        if (toOpacity == Opacity.Cutout) {
            --propagatedRedValue;
            --propagatedGreenValue;
            --propagatedBlueValue;
            --propagatedSkyValue;
        }
        if (propagatedRedValue < 1 && propagatedGreenValue < 1 && propagatedBlueValue < 1 && propagatedSkyValue < 1) {
            return;
        }
        final short oldLightValue = toLight.getLightRaw(toBlockIndex);
        byte neighbourRedLight = ChunkLightData.getLightValue(oldLightValue, 0);
        byte neighbourGreenLight = ChunkLightData.getLightValue(oldLightValue, 1);
        byte neighbourBlueLight = ChunkLightData.getLightValue(oldLightValue, 2);
        byte neighbourSkyLight = ChunkLightData.getLightValue(oldLightValue, 3);
        if (neighbourRedLight < propagatedRedValue) {
            neighbourRedLight = propagatedRedValue;
        }
        if (neighbourGreenLight < propagatedGreenValue) {
            neighbourGreenLight = propagatedGreenValue;
        }
        if (neighbourBlueLight < propagatedBlueValue) {
            neighbourBlueLight = propagatedBlueValue;
        }
        if (neighbourSkyLight < propagatedSkyValue) {
            neighbourSkyLight = propagatedSkyValue;
        }
        short newLightValue = (short)((neighbourRedLight & 0xF) << 0);
        newLightValue |= (short)((neighbourGreenLight & 0xF) << 4);
        newLightValue |= (short)((neighbourBlueLight & 0xF) << 8);
        newLightValue |= (short)((neighbourSkyLight & 0xF) << 12);
        toLight.setLightRaw(toBlockIndex, newLightValue);
        if (newLightValue != oldLightValue && (propagatedRedValue > 1 || propagatedGreenValue > 1 || propagatedBlueValue > 1 || propagatedSkyValue > 1)) {
            bitSetQueue.set(toBlockIndex);
        }
    }
}
