/*
 *   This file is a part of the SteamWar software.
 *
 *   Copyright (C) 2025 SteamWar.de-Serverteam
 *
 *   This program is free software: you can redistribute it and/or modify
 *   it under the terms of the GNU Affero General Public License as published by
 *   the Free Software Foundation, either version 3 of the License, or
 *   (at your option) any later version.
 *
 *   This program is distributed in the hope that it will be useful,
 *   but WITHOUT ANY WARRANTY; without even the implied warranty of
 *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 *   GNU Affero General Public License for more details.
 *
 *   You should have received a copy of the GNU Affero General Public License
 *   along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */

package de.steamwar.techhider.legacy;

import de.steamwar.techhider.ProtocolUtils;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import lombok.Getter;
import net.minecraft.core.registries.BuiltInRegistries;
import net.minecraft.network.protocol.game.ClientboundLevelChunkPacketData;
import net.minecraft.network.protocol.game.ClientboundLevelChunkWithLightPacket;
import net.minecraft.util.SimpleBitStorage;
import net.minecraft.world.level.block.entity.BlockEntityType;
import org.bukkit.entity.Player;

import java.util.Collections;
import java.util.Set;
import java.util.function.BiFunction;
import java.util.function.UnaryOperator;
import java.util.stream.Collectors;

@Deprecated
public class ChunkHider {
    public static final ChunkHider impl = new ChunkHider();

    private static final UnaryOperator<ClientboundLevelChunkWithLightPacket> chunkPacketCloner = ProtocolUtils.shallowCloneGenerator(ClientboundLevelChunkWithLightPacket.class, ClientboundLevelChunkWithLightPacket::new);
    private static final UnaryOperator<ClientboundLevelChunkPacketData> chunkDataCloner = ProtocolUtils.shallowCloneGenerator(ClientboundLevelChunkPacketData.class, ClientboundLevelChunkPacketData::new);

    public BiFunction<Player, ClientboundLevelChunkWithLightPacket, ClientboundLevelChunkWithLightPacket> chunkHiderGenerator(TechHider techHider) {
        return (p, packet) -> {
            int chunkX = packet.getX();
            int chunkZ = packet.getZ();
            if (techHider.getLocationEvaluator().skipChunk(p, chunkX, chunkZ)) {
                return packet;
            }

            packet = chunkPacketCloner.apply(packet);
            ClientboundLevelChunkPacketData dataWrapper = chunkDataCloner.apply(packet.getChunkData());

            Set<String> hiddenBlockEntities = techHider.getHiddenBlockEntities();
            dataWrapper.blockEntitiesData = dataWrapper.blockEntitiesData.stream()
                    .filter(te -> tileEntityVisible(hiddenBlockEntities, te))
                    .collect(Collectors.toList());

            ByteBuf in = Unpooled.wrappedBuffer(dataWrapper.buffer);
            ByteBuf out = Unpooled.buffer(in.readableBytes() + 64);
            for (int yOffset = p.getWorld().getMinHeight(); yOffset < p.getWorld().getMaxHeight(); yOffset += 16) {
                SectionHider section = new SectionHider(p, techHider, in, out, chunkX, yOffset / 16, chunkZ);
                section.copyBlockCount();

                blocks(section);
                biomes(section);
            }

            if (in.readableBytes() != 0) {
                throw new IllegalStateException("ChunkHider21: Incomplete chunk data, " + in.readableBytes() + " bytes left");
            }

            byte[] data = new byte[out.readableBytes()];
            out.readBytes(data);
            dataWrapper.buffer = data;

            packet.chunkData = dataWrapper;
            return packet;
        };
    }

    protected boolean tileEntityVisible(Set<String> hiddenBlockEntities, ClientboundLevelChunkPacketData.BlockEntityInfo tile) {
        BlockEntityType<?> type = tile.type;
        String path = BuiltInRegistries.BLOCK_ENTITY_TYPE.getKey(type).getPath();
        return !hiddenBlockEntities.contains(path);
    }

    private void blocks(SectionHider section) {
        section.copyBitsPerBlock();

        boolean singleValued = section.getBitsPerBlock() == 0;
        if (singleValued) {
            int value = ProtocolUtils.readVarInt(section.getIn());
            ProtocolUtils.writeVarInt(section.getOut(), !section.isSkipSection() && section.getObfuscate().contains(value) ? section.getTarget() : value);
            return;
        } else if (section.getBitsPerBlock() < 9) {
            // Indirect (paletted) storage – only present when bitsPerBlock < 9 in 1.21+
            section.processPalette();
        }

        if (section.isSkipSection() || (!section.blockPrecise() && section.isPaletted())) {
            section.skipNewDataArray(4096);
            return;
        }

        SimpleBitStorage values = new SimpleBitStorage(section.getBitsPerBlock(), 4096, section.readNewDataArray(4096));

        for (int y = 0; y < 16; y++) {
            for (int z = 0; z < 16; z++) {
                for (int x = 0; x < 16; x++) {
                    int pos = (((y * 16) + z) * 16) + x;

                    TechHider.State test = section.test(x, y, z);

                    switch (test) {
                        case SKIP:
                            break;
                        case CHECK:
                            if (!section.getObfuscate().contains(values.get(pos))) {
                                break;
                            }
                        case HIDE:
                            values.set(pos, section.getTarget());
                            break;
                        case HIDE_AIR:
                        default:
                            values.set(pos, section.getAir());
                    }
                }
            }
        }

        section.writeDataArray(values.getRaw());
    }

    private void biomes(SectionHider section) {
        section.copyBitsPerBlock();
        if (section.getBitsPerBlock() == 0) {
            section.copyVarInt();
        } else if (section.getBitsPerBlock() < 6) {
            section.skipPalette();
            section.skipNewDataArray(64);
        } else {
            // Direct (global) biome IDs – no palette present
            section.skipNewDataArray(64);
        }
    }

    @Getter
    class SectionHider {
        private final Player player;
        private final TechHider techHider;
        private final ByteBuf in;
        private final ByteBuf out;

        private final int chunkX;
        private final int chunkY;
        private final int chunkZ;
        private final int offsetX;
        private final int offsetY;
        private final int offsetZ;

        private final boolean skipSection;

        private boolean paletted;
        private int bitsPerBlock;
        private int blockCount;
        private int air;
        private int target;
        private Set<Integer> obfuscate;

        public SectionHider(Player player, TechHider techHider, ByteBuf in, ByteBuf out, int chunkX, int chunkY, int chunkZ) {
            this.player = player;
            this.techHider = techHider;
            this.in = in;
            this.out = out;
            this.chunkX = chunkX;
            this.chunkY = chunkY;
            this.chunkZ = chunkZ;
            this.offsetX = 16 * chunkX;
            this.offsetY = 16 * chunkY;
            this.offsetZ = 16 * chunkZ;
            this.skipSection = techHider.getLocationEvaluator().skipChunkSection(player, chunkX, chunkY, chunkZ);

            this.paletted = false;
            this.bitsPerBlock = 0;
            this.air = TechHider.AIR_ID;
            this.target = techHider.getObfuscationTargetId();
            this.obfuscate = techHider.getObfuscateIds();
        }

        public boolean blockPrecise() {
            return techHider.getLocationEvaluator().blockPrecise(player, chunkX, chunkY, chunkZ);
        }

        public TechHider.State test(int x, int y, int z) {
            return techHider.getLocationEvaluator().check(player, offsetX + x, offsetY + y, offsetZ + z);
        }

        public void copyBlockCount() {
            this.blockCount = in.readShort();
            out.writeShort(blockCount);
        }

        public void copyBitsPerBlock() {
            bitsPerBlock = in.readByte();
            out.writeByte(bitsPerBlock);
        }

        public int copyVarInt() {
            int value = ProtocolUtils.readVarInt(in);
            ProtocolUtils.writeVarInt(out, value);
            return value;
        }

        public void skipPalette() {
            int paletteLength = copyVarInt();
            for (int i = 0; i < paletteLength; i++) {
                copyVarInt();
            }
        }

        public void processPalette() {
            if (skipSection) {
                skipPalette();
                return;
            }

            int paletteLength = copyVarInt();
            if (paletteLength == 0) return;

            paletted = true;
            air = 0;
            target = 0;

            for (int i = 0; i < paletteLength; i++) {
                int entry = ProtocolUtils.readVarInt(in);
                if (obfuscate.contains(entry)) {
                    entry = techHider.getObfuscationTargetId();
                }

                if (entry == TechHider.AIR_ID) {
                    air = i;
                } else if (entry == techHider.getObfuscationTargetId()) {
                    target = i;
                }

                ProtocolUtils.writeVarInt(out, entry);
            }
            obfuscate = Collections.emptySet();
        }

        public void skipDataArray() {
            int dataArrayLength = copyVarInt();
            out.writeBytes(in, dataArrayLength * 8);
        }

        public void skipNewDataArray(int entries) {
            if (bitsPerBlock == 0) {
                return;
            }

            char valuesPerLong = (char) (64 / bitsPerBlock);
            int i1 = (entries + valuesPerLong - 1) / valuesPerLong;
            out.writeBytes(in, i1 * Long.BYTES);
        }

        public long[] readDataArray() {
            long[] array = new long[copyVarInt()];
            for (int i = 0; i < array.length; i++) {
                array[i] = in.readLong();
            }

            return array;
        }

        public long[] readNewDataArray(int entries) {
            if (bitsPerBlock == 0) {
                return new long[entries];
            }

            char valuesPerLong = (char) (64 / bitsPerBlock);
            int i1 = (entries + valuesPerLong - 1) / valuesPerLong;
            long[] array = new long[i1];
            for (int i = 0; i < i1; i++) {
                array[i] = in.readLong();
            }

            return array;
        }

        public void writeDataArray(long[] array) {
            for (long l : array) {
                out.writeLong(l);
            }
        }
    }
}
