package org.allaymc.server.world.light;

import com.google.common.base.Preconditions;
import io.netty.util.internal.PlatformDependent;
import it.unimi.dsi.fastutil.longs.LongOpenHashSet;
import org.allaymc.api.block.data.BlockFace;
import org.allaymc.api.math.MathUtils;
import org.allaymc.api.utils.hash.HashUtils;
import org.allaymc.api.world.Dimension;
import org.allaymc.api.world.chunk.Chunk;
import org.allaymc.api.world.chunk.OperationType;
import org.allaymc.api.world.chunk.UnsafeChunk;
import org.allaymc.api.world.data.Weather;
import org.allaymc.api.world.dimension.DimensionType;
import org.allaymc.api.world.light.LightEngine;
import org.allaymc.server.AllayServer;
import org.allaymc.server.datastruct.ChunkSectionNibbleArray;
import org.allaymc.server.datastruct.collections.queue.BlockingQueueWrapper;
import org.allaymc.server.world.chunk.AllayChunkSection;
import org.allaymc.server.world.chunk.HeightMap;
import org.jctools.maps.NonBlockingHashMapLong;
import org.jctools.maps.NonBlockingHashSet;
import org.jetbrains.annotations.Range;
import org.jetbrains.annotations.VisibleForTesting;

import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Supplier;

/**
 * @author daoge_cmd
 */
public class AllayLightEngine implements LightEngine {

    protected final DimensionType dimensionType;
    protected final String worldName;
    protected final AtomicBoolean isRunning;
    protected final Supplier<Integer> timeSupplier;
    protected final Supplier<Weather> weatherSupplier;
    protected final int maxUpdateCount;
    protected final boolean useVirtualThread;
    protected final BlockingQueueWrapper<Runnable> chunkAndBlockUpdateQueue;
    protected final BlockingQueueWrapper<Runnable> blockLightUpdateQueue;
    protected final Set<Long> chunks;
    /**
     * Stores the chunks that are loaded but haven't calculated the light yet, because one
     * or more of their neighbor chunks are not loaded. Further block changes in these chunks
     * will be ignored since we will calculate the light in the whole chunk later once their
     * neighbor chunks are loaded.
     */
    protected final Set<Long> awaitingLightCalculationChunks;
    protected final NonBlockingHashMapLong<ChunkSectionNibbleArray[]> lightDampening;
    protected final NonBlockingHashMapLong<ChunkSectionNibbleArray[]> lightEmission;
    protected final NonBlockingHashMapLong<ChunkSectionNibbleArray[]> blockLight;
    protected final LightPropagator blockLightPropagator;
    /**
     * The light data accessor used in "Chunk & Block" thread. We use LightDataAccessor as much
     * as possible, because the implementation of LightDataAccessor will cache the last reading
     * chunk and avoid getting chunk from large hash table every time (which will be much faster)
     * To avoid race conditions, we limit the read and write threads for various data:
     * Light (Sky Light) - Sky Light Calculating Thread
     * Light Dampening&Emission - Chunk & Block Light Calculation Thread
     */
    protected final CachedLightDataAccessor lightDataAccessor;

    /*
     * The following fields are not null only when the dimension has sky light
     */

    protected BlockingQueueWrapper<Runnable> skyLightUpdateQueue;
    /**
     * Store the sky light sources' height in each chunk
     */
    protected NonBlockingHashMapLong<HeightMap> lightHeightMap;
    /**
     * Use CachedMapAccessor to get light height for better performance
     */
    protected CachedChunkMapAccessor<HeightMap> lightHeightMapAccessor;
    /**
     * This map only contains the propagation result of sky light sources that are in the
     * border (at critical light height or horizontal propagation needs to be considered).
     * Skylight sources have a value of 15 in this map
     */
    protected NonBlockingHashMapLong<ChunkSectionNibbleArray[]> skyLightInBorder;
    protected LightPropagator skyLightPropagator;

    public AllayLightEngine(Dimension dimension, boolean useVirtualThread) {
        this(dimension.getDimensionType(), dimension.getWorld().getName(), dimension.getWorld().getWorldData()::getTimeOfDay, dimension.getWorld()::getWeather, AllayServer.getSettings().worldSettings().maxLightUpdateCountPerDimension(), useVirtualThread);
    }

    @VisibleForTesting
    public AllayLightEngine(DimensionType dimensionType, String worldName, Supplier<Integer> timeSupplier, Supplier<Weather> weatherSupplier) {
        this(dimensionType, worldName, timeSupplier, weatherSupplier, Integer.MAX_VALUE, false);
    }

    protected AllayLightEngine(DimensionType dimensionType, String worldName, Supplier<Integer> timeSupplier, Supplier<Weather> weatherSupplier, int maxUpdateCount, boolean useVirtualThread) {
        this.dimensionType = dimensionType;
        this.worldName = worldName;
        this.isRunning = new AtomicBoolean(true);
        this.timeSupplier = timeSupplier;
        this.weatherSupplier = weatherSupplier;
        this.maxUpdateCount = maxUpdateCount;
        this.useVirtualThread = useVirtualThread;
        this.chunkAndBlockUpdateQueue = BlockingQueueWrapper.wrap(PlatformDependent.newMpscQueue());
        this.blockLightUpdateQueue = BlockingQueueWrapper.wrap(PlatformDependent.newMpscQueue());
        this.chunks = new NonBlockingHashSet<>();
        // Will only be read and wrote in chunk & block update thread, so no need to use a thread-safe set
        this.awaitingLightCalculationChunks = new LongOpenHashSet();
        this.lightDampening = new NonBlockingHashMapLong<>();
        this.lightEmission = new NonBlockingHashMapLong<>();
        this.blockLight = new NonBlockingHashMapLong<>();
        this.blockLightPropagator = new LightPropagator(new CachedLightDataAccessor(dimensionType, blockLight, lightDampening, lightEmission));
        if (dimensionType.hasSkyLight()) {
            this.skyLightUpdateQueue = BlockingQueueWrapper.wrap(PlatformDependent.newMpscQueue());
            this.lightHeightMap = new NonBlockingHashMapLong<>();
            this.lightHeightMapAccessor = new CachedChunkMapAccessor<>(lightHeightMap);
            this.skyLightInBorder = new NonBlockingHashMapLong<>();
            this.skyLightPropagator = new LightPropagator(new CachedLightDataAccessor(dimensionType, skyLightInBorder, lightDampening, lightEmission));
        }
        // Initialize lightDataAccessor after we initialized skylight related things
        // NOTICE: we only cache skyLightInBorder, block light is not much used in "Chunk & Block" thread
        this.lightDataAccessor = new CachedLightDataAccessor(dimensionType, skyLightInBorder, lightDampening, lightEmission);
    }

    @VisibleForTesting
    public static int calculateSkylightReduction(long time, Weather weather) {
        double d = 1.0 - ((weather != Weather.CLEAR ? 1 : 0) * 5.0) / 16.0;
        double e = 1.0 - ((weather == Weather.THUNDER ? 1 : 0) * 5.0) / 16.0;
        double f = 0.5 + 2.0 * Math.clamp(MathUtils.fastCos(calculateCelestialAngle(time) * 6.2831855), -0.25, 0.25);
        return (int) ((1.0 - f * d * e) * 11.0);
    }

    @VisibleForTesting
    public static double calculateCelestialAngle(long time) {
        double d = frac(((double) time) / 24000.0 - 0.25);
        double e = 0.5 - Math.cos(d * Math.PI) / 2.0;
        return (d * 2.0 + e) / 3.0F;
    }

    protected static double frac(double value) {
        return value - (double) lfloor(value);
    }

    protected static long lfloor(double value) {
        long l = (long) value;
        return value < (double) l ? l - 1L : l;
    }

    protected static byte packLightData(@Range(from = 0, to = 16) int lightEmission, @Range(from = 0, to = 16) int lightDampening) {
        Preconditions.checkArgument(lightEmission < 16, "lightEmission must be less than 16");
        Preconditions.checkArgument(lightDampening < 16, "lightDampening must be less than 16");
        return (byte) (lightEmission | (lightDampening << 4));
    }

    protected static byte unpackLightEmission(byte data) {
        return (byte) (data & 0x0F);
    }

    protected static byte unpackLightDampening(byte data) {
        return (byte) ((data >> 4) & 0x0F);
    }

    public void startTick() {
        var dimensionName = worldName + ":" + dimensionType.getIdentifier();
        startCalculatingThread("Light Calculating Thread (Chunk & Block) #" + dimensionName, chunkAndBlockUpdateQueue);
        startCalculatingThread("Light Calculating Thread (Block Light) #" + dimensionName, blockLightUpdateQueue);
        if (dimensionType.hasSkyLight()) {
            startCalculatingThread("Light Calculating Thread (Sky Light) #" + dimensionName, skyLightUpdateQueue);
        }
    }

    public void shutdown() {
        this.isRunning.set(false);
    }

    protected void startCalculatingThread(String name, BlockingQueueWrapper<Runnable> queue) {
        (useVirtualThread ? Thread.ofVirtual() : Thread.ofPlatform()).name(name).start(() -> {
            while (isRunning.get()) {
                this.handleUpdateIn(queue);
            }
        });
    }

    protected void handleUpdateIn(BlockingQueueWrapper<Runnable> queue) {
        Runnable runnable;
        while ((runnable = queue.tryPoll(1, TimeUnit.SECONDS)) != null) {
            runnable.run();
        }
    }

    protected void handleUpdateUninterruptibleIn(BlockingQueueWrapper<Runnable> queue) {
        Runnable runnable;
        while ((runnable = queue.pollNow()) != null) {
            runnable.run();
        }
    }

    @VisibleForTesting
    public void handleUpdateInAllQueues() {
        handleUpdateUninterruptibleIn(chunkAndBlockUpdateQueue);
        handleUpdateUninterruptibleIn(blockLightUpdateQueue);
        if (dimensionType.hasSkyLight()) {
            handleUpdateUninterruptibleIn(skyLightUpdateQueue);
        }
    }

    protected void addChunk(UnsafeChunk chunk) {
        var chunkLightDampening = createNibbleArrays();
        var chunkLightEmission = createNibbleArrays();
        var chunkLightHeightMap = dimensionType.hasSkyLight() ? new HeightMap((short) dimensionType.getMinHeight()) : null;
        for (int y = dimensionType.getMaxHeight(); y >= dimensionType.getMinHeight(); y--) {
            var section = (AllayChunkSection) chunk.getSection(y >> 4);
            for (int x = 0; x < 16; x++) {
                for (int z = 0; z < 16; z++) {
                    var blockStateData = section.getBlockState(x, y & 0xf, z, 0).getBlockStateData();
                    var lightDampening = blockStateData.lightDampening();
                    var lightEmission = blockStateData.lightEmission();

                    if (lightDampening != 0) {
                        chunkLightDampening[(y - dimensionType.getMinHeight()) >> 4].set(x, y & 0xf, z, lightDampening);
                        boolean skyLightSourceNotFound = dimensionType.hasSkyLight() && chunkLightHeightMap.get(x, z) == dimensionType.getMinHeight();
                        if (skyLightSourceNotFound) {
                            chunkLightHeightMap.set(x, z, (short) (y + 1));
                        }
                    }

                    if (lightEmission != 0) {
                        chunkLightEmission[(y - dimensionType.getMinHeight()) >> 4].set(x, y & 0xf, z, lightEmission);
                    }
                }
            }
        }

        var hash = HashUtils.hashXZ(chunk.getX(), chunk.getZ());
        lightDampening.put(hash, chunkLightDampening);
        lightEmission.put(hash, chunkLightEmission);
        blockLight.put(hash, createNibbleArrays());
        if (dimensionType.hasSkyLight()) {
            lightHeightMap.put(hash, chunkLightHeightMap);
            skyLightInBorder.put(hash, createNibbleArrays());
        }
        chunks.add(hash);
    }

    protected void tryCalculateChunkLightAt(int chunkX, int chunkZ, Runnable afterCalculated) {
        if (this.getQueuedUpdateCount() > maxUpdateCount) {
            // We have too many light updates in the queue, so we need to wait for a while
            chunkAndBlockUpdateQueue.offer(() -> {
                // Recheck is needed since there may be chunk load/unload
                if (canCalculateLightInChunk(chunkX, chunkZ)) {
                    tryCalculateChunkLightAt(chunkX, chunkZ, afterCalculated);
                }
            });
            return;
        }

        for (int worldY = dimensionType.getMaxHeight(); worldY >= dimensionType.getMinHeight(); worldY--) {
            for (int x = 0; x < 16; x++) {
                for (int z = 0; z < 16; z++) {
                    final int worldX = (chunkX << 4) + x;
                    final int worldZ = (chunkZ << 4) + z;
                    calculateBlockLightAt(worldX, worldY, worldZ);
                    if (dimensionType.hasSkyLight()) {
                        calculateSkyLightAt(worldX, worldZ);
                    }
                }
            }
        }

        if (afterCalculated != null) {
            afterCalculated.run();
        }
    }

    protected void calculateBlockLightAt(int x, int y, int z) {
        // Cast to byte to save memory
        byte lightEmissionValue = (byte) lightDataAccessor.getLightEmission(x, y, z);
        int minNeighborLightEmission = 15;
        if (lightEmissionValue != 0) {
            var ignoreCurrentBlockLight = false;
            for (var blockFace : BlockFace.VALUES) {
                // The following are world pos not chunk local pos
                var neighborX = x + blockFace.getOffset().x();
                var neighborZ = z + blockFace.getOffset().z();
                var neighborY = y + blockFace.getOffset().y();
                if (!lightDataAccessor.isYInRange(neighborY)) {
                    continue;
                }

                int neighborLightEmission = lightDataAccessor.getLightEmission(neighborX, neighborY, neighborZ);
                if (neighborLightEmission > lightEmissionValue) {
                    // We can ignore the current block, because the neighbor block's light will cover the current block
                    ignoreCurrentBlockLight = true;
                    break;
                }

                minNeighborLightEmission = Math.min(minNeighborLightEmission, neighborLightEmission);
            }

            // Check if the current block is surrounded with blocks that have the same light
            // emission, usually happen in lava pool where have a lot of lava blocks inside
            if (minNeighborLightEmission == lightEmissionValue) {
                setWithoutChunkCheck(blockLight, x, y, z, lightEmissionValue);
                ignoreCurrentBlockLight = true;
            }

            if (!ignoreCurrentBlockLight) {
                blockLightUpdateQueue.offer(() -> blockLightPropagator.setLightAndPropagate(x, y, z, 0, lightEmissionValue));
            }
        }
    }

    protected void calculateSkyLightAt(int x, int z) {
        var lightHeight = getLightHeight(x, z);
        if (lightHeight > dimensionType.getMaxHeight()) {
            // This x-z position is full of blocks
            return;
        }

        var maxNeighborLightHeight = getMaxNeighborLightHeight(x, z);
        for (int i = lightHeight; i <= maxNeighborLightHeight; i++) {
            final int skyLightSourceY = i;
            skyLightUpdateQueue.offer(() -> skyLightPropagator.setLightAndPropagate(x, skyLightSourceY, z, 0, 15));
        }
    }

    protected int getMaxNeighborLightHeight(int x, int z) {
        int max = Integer.MIN_VALUE;
        BlockFace[] horizontalBlockFaces = BlockFace.getHorizontalBlockFaces();
        for (BlockFace face : horizontalBlockFaces) {
            max = Math.max(max, getLightHeight(x + face.getOffset().x(), z + face.getOffset().z()));
        }

        return max;
    }

    protected boolean canCalculateLightInChunk(int chunkX, int chunkZ) {
        for (int i = -1; i <= 1; i++) {
            for (int j = -1; j <= 1; j++) {
                if (!chunks.contains(HashUtils.hashXZ(chunkX + i, chunkZ + j))) {
                    return false;
                }
            }
        }

        return true;
    }

    protected ChunkSectionNibbleArray[] createNibbleArrays() {
        ChunkSectionNibbleArray[] nibbleArrays = new ChunkSectionNibbleArray[dimensionType.chunkSectionCount()];
        for (int i = 0; i < dimensionType.chunkSectionCount(); i++) {
            nibbleArrays[i] = new ChunkSectionNibbleArray();
        }
        return nibbleArrays;
    }

    @Override
    public int getSkyLight(int x, int y, int z) {
        if (!dimensionType.hasSkyLight()) {
            return 0;
        }

        if (!chunks.contains(HashUtils.hashXZ(x >> 4, z >> 4))) {
            // Chunk is not loaded
            return 15;
        }

        var lightHeight = getLightHeight(x, z);
        if (y >= lightHeight) {
            return 15;
        }

        return getWithoutChunkCheck(skyLightInBorder, x, y, z);
    }

    protected short getLightHeight(int x, int z) {
        return lightHeightMapAccessor.get(x, z).get(x & 15, z & 15);
    }

    protected void setLightHeight(int x, int z, short value) {
        lightHeightMapAccessor.get(x, z).set(x & 15, z & 15, value);
    }

    @Override
    public int getBlockLight(int x, int y, int z) {
        return get(blockLight, x, y, z, 0);
    }

    @Override
    public int getInternalLight(int x, int y, int z) {
        return dimensionType.hasSkyLight() ? Math.max(getInternalSkyLight(x, y, z), getBlockLight(x, y, z)) : getBlockLight(x, y, z);
    }

    @Override
    public int getInternalSkyLight(int x, int y, int z) {
        return dimensionType.hasSkyLight() ? Math.max(0, getSkyLight(x, y, z) - calculateSkylightReduction(timeSupplier.get(), weatherSupplier.get())) : 0;
    }

    public void onBlockChange(int x, int y, int z, int le, int ld) {
        if (!isRunning.get()) {
            return;
        }

        // Reduce memory usage by packing light data into a single byte
        var packedLightData = packLightData(le, ld);
        chunkAndBlockUpdateQueue.offer(() -> {
            var chunkHash = HashUtils.hashXZ(x >> 4, z >> 4);
            if (!chunks.contains(chunkHash)) {
                // Chunk is not loaded
                return;
            }

            var lightDampening = unpackLightDampening(packedLightData);
            var lightEmissionValue = unpackLightEmission(packedLightData);
            int oldLightHeight = dimensionType.hasSkyLight() ? getLightHeight(x, z) : 0;
            var oldBlockDampening = lightDataAccessor.getLightDampening(x, y, z);
            var oldBlockEmission = lightDataAccessor.getLightEmission(x, y, z);

            if (oldBlockDampening != lightDampening) {
                setLightDampening(x, y, z, lightDampening);
            }

            if (oldBlockEmission != lightEmissionValue) {
                lightDataAccessor.setLightEmission(x, y, z, lightEmissionValue);
            }

            var chunkLightCalculated = !awaitingLightCalculationChunks.contains(chunkHash);
            if (!chunkLightCalculated || (oldBlockDampening == lightDampening && oldBlockEmission == lightEmissionValue)) {
                return;
            }

            blockLightUpdateQueue.offer(() -> blockLightPropagator.setLightAndPropagate(x, y, z, getBlockLight(x, y, z), lightEmissionValue));
            if (!dimensionType.hasSkyLight() || oldBlockDampening == lightDampening) {
                // No change in light dampening, so no need to recalculate the sky light because
                // sky light is only related to block dampening
                return;
            }

            var newLightHeight = getLightHeight(x, z);
            if (newLightHeight == oldLightHeight) {
                // No change to light height, only need to make sure the sky light can be re-propagated to the changed block's position
                skyLightUpdateQueue.offer(() -> skyLightPropagator.setLightAndPropagate(x, y, z, lightDataAccessor.getLight(x, y, z), 0));
            } else {
                var min = Math.min(oldLightHeight, newLightHeight);
                var max = Math.max(oldLightHeight, newLightHeight);

                // Remove old skylight sources responsible for horizontal skylight propagation
                for (int i = min; i <= max; i++) {
                    var skyLightSourceY = i;
                    skyLightUpdateQueue.offer(() -> {
                        if (lightDataAccessor.getLight(x, skyLightSourceY, z) == 15) {
                            skyLightPropagator.setLightAndPropagate(x, skyLightSourceY, z, 15, 0);
                        }
                    });
                    for (var face : BlockFace.getHorizontalBlockFaces()) {
                        var ox = x + face.getOffset().x();
                        var oz = z + face.getOffset().z();
                        skyLightUpdateQueue.offer(() -> {
                            if (lightDataAccessor.getLight(ox, skyLightSourceY, oz) == 15) {
                                skyLightPropagator.setLightAndPropagate(ox, skyLightSourceY, oz, 15, 0);
                            }
                        });
                    }
                }

                // Recalculate skylight
                calculateSkyLightAt(x, z);
                for (var face : BlockFace.getHorizontalBlockFaces()) {
                    var neighborX = x + face.getOffset().x();
                    var neighborZ = z + face.getOffset().z();
                    calculateSkyLightAt(neighborX, neighborZ);
                }
            }
        });
    }

    public void onChunkLoad(Chunk chunk) {
        if (!isRunning.get()) {
            return;
        }

        chunkAndBlockUpdateQueue.offer(() -> {
            chunk.applyOperation(this::addChunk, OperationType.READ, OperationType.NONE);

            // Check if we can calculate the light in this chunk immediately
            if (canCalculateLightInChunk(chunk.getX(), chunk.getZ())) {
                tryCalculateChunkLightAt(chunk.getX(), chunk.getZ(), null);
            } else {
                awaitingLightCalculationChunks.add(HashUtils.hashXZ(chunk.getX(), chunk.getZ()));
            }

            // Check if the neighbor chunks' light can be calculated since we have a
            // new chunk added (if the neighbor chunks' light haven't been calculated)
            for (int i = -1; i <= 1; i++) {
                for (int j = -1; j <= 1; j++) {
                    if (i == 0 && j == 0) {
                        continue;
                    }

                    var neighborChunkHash = HashUtils.hashXZ(chunk.getX() + i, chunk.getZ() + j);
                    if (awaitingLightCalculationChunks.contains(neighborChunkHash)) {
                        if (canCalculateLightInChunk(chunk.getX() + i, chunk.getZ() + j)) {
                            tryCalculateChunkLightAt(chunk.getX() + i, chunk.getZ() + j, () -> {
                                awaitingLightCalculationChunks.remove(neighborChunkHash);
                            });
                        }
                    }
                }
            }
        });
    }

    public void onChunkUnload(Chunk chunk) {
        if (!isRunning.get()) {
            return;
        }

        chunkAndBlockUpdateQueue.offer(() -> {
            var hash = HashUtils.hashXZ(chunk.getX(), chunk.getZ());
            lightDampening.remove(hash);
            lightEmission.remove(hash);
            blockLight.remove(hash);
            if (dimensionType.hasSkyLight()) {
                lightHeightMap.remove(hash);
                skyLightInBorder.remove(hash);
            }
            chunks.remove(hash);
            awaitingLightCalculationChunks.remove(hash);

            // Add the loaded neighbor chunk to the awaiting light calculation chunks, since
            // their neighbor chunk has been unloaded. Once the neighbor chunk is loaded back,
            // the light in these chunks will be recalculated
            for (int i = -1; i <= 1; i++) {
                for (int j = -1; j <= 1; j++) {
                    if (i == 0 && j == 0) {
                        continue;
                    }

                    var neighborChunkHash = HashUtils.hashXZ(chunk.getX() + i, chunk.getZ() + j);
                    if (chunks.contains(neighborChunkHash) && !awaitingLightCalculationChunks.contains(neighborChunkHash)) {
                        awaitingLightCalculationChunks.add(neighborChunkHash);
                        resetBlockAndSkyLightInChunk(neighborChunkHash);
                    }
                }
            }
        });
    }

    protected void resetBlockAndSkyLightInChunk(long hash) {
        for (var array : blockLight.get(hash)) {
            array.reset();
        }
    }

    @Override
    public int getQueuedUpdateCount() {
        return chunkAndBlockUpdateQueue.size() + blockLightUpdateQueue.size() + (skyLightUpdateQueue != null ? skyLightUpdateQueue.size() : 0);
    }

    protected int get(NonBlockingHashMapLong<ChunkSectionNibbleArray[]> target, int x, int y, int z, int defaultValue) {
        var hash = HashUtils.hashXZ(x >> 4, z >> 4);
        if (!chunks.contains(hash)) {
            return defaultValue;
        }
        var array = target.get(hash);
        return array[(y - dimensionType.getMinHeight()) >> 4].get(x & 15, y & 15, z & 15);
    }

    protected int getWithoutChunkCheck(NonBlockingHashMapLong<ChunkSectionNibbleArray[]> target, int x, int y, int z) {
        var hash = HashUtils.hashXZ(x >> 4, z >> 4);
        var array = target.get(hash);
        return array[(y - dimensionType.getMinHeight()) >> 4].get(x & 15, y & 15, z & 15);
    }

    protected void set(NonBlockingHashMapLong<ChunkSectionNibbleArray[]> target, int x, int y, int z, int value) {
        var hash = HashUtils.hashXZ(x >> 4, z >> 4);
        if (!chunks.contains(hash)) {
            return;
        }
        var array = target.get(hash);
        array[(y - dimensionType.getMinHeight()) >> 4].set(x & 15, y & 15, z & 15, value);
    }

    protected void setWithoutChunkCheck(NonBlockingHashMapLong<ChunkSectionNibbleArray[]> target, int x, int y, int z, int value) {
        var hash = HashUtils.hashXZ(x >> 4, z >> 4);
        var array = target.get(hash);
        array[(y - dimensionType.getMinHeight()) >> 4].set(x & 15, y & 15, z & 15, value);
    }

    protected void setLightDampening(int x, int y, int z, int value) {
        setWithoutChunkCheck(lightDampening, x, y, z, value);
        if (!dimensionType.hasSkyLight()) {
            return;
        }

        // Update light height
        var currentLightHeight = getLightHeight(x, z);
        if (value == 0 && currentLightHeight >= y + 1) {
            short newLightHeight = (short) dimensionType.getMinHeight();
            for (short i = currentLightHeight; i >= dimensionType.getMinHeight(); i--) {
                if (lightDataAccessor.getLightDampening(x, i, z) != 0) {
                    newLightHeight = (short) (i + 1);
                    break;
                }
            }
            setLightHeight(x, z, newLightHeight);
        } else if (currentLightHeight <= y) {
            setLightHeight(x, z, (short) (y + 1));
        }
    }
}
