/*
    This file is a part of the SteamWar software.

    Copyright (C) 2021  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;

import de.steamwar.Reflection;
import de.steamwar.core.Core;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import net.minecraft.core.IRegistry;
import net.minecraft.network.protocol.game.ClientboundLevelChunkPacketData;
import net.minecraft.network.protocol.game.ClientboundLevelChunkWithLightPacket;
import net.minecraft.resources.MinecraftKey;
import net.minecraft.util.SimpleBitStorage;
import net.minecraft.world.level.block.entity.TileEntityTypes;
import org.bukkit.entity.Player;

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

public class ChunkHider18 implements ChunkHider {
    @Override
    public Class<?> mapChunkPacket() {
        return ClientboundLevelChunkWithLightPacket.class;
    }

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

    private static final Reflection.Field<Integer> chunkXField = Reflection.getField(ClientboundLevelChunkWithLightPacket.class, int.class, 0);
    private static final Reflection.Field<Integer> chunkZField = Reflection.getField(ClientboundLevelChunkWithLightPacket.class, int.class, 1);
    private static final Reflection.Field<ClientboundLevelChunkPacketData> chunkData = Reflection.getField(ClientboundLevelChunkWithLightPacket.class, ClientboundLevelChunkPacketData.class, 0);

    private static final Reflection.Field<byte[]> dataField = Reflection.getField(ClientboundLevelChunkPacketData.class, byte[].class, 0);
    private static final Reflection.Field<List> tileEntities = Reflection.getField(ClientboundLevelChunkPacketData.class, List.class, 0);

    @Override
    public BiFunction<Player, Object, Object> chunkHiderGenerator(TechHider techHider) {
        return (p, packet) -> {
            int chunkX = chunkXField.get(packet);
            int chunkZ = chunkZField.get(packet);
            if (techHider.getLocationEvaluator().skipChunk(p, chunkX, chunkZ))
                return packet;

            packet = chunkPacketCloner.apply(packet);
            Object dataWrapper = chunkDataCloner.apply(chunkData.get(packet));

            Set<String> hiddenBlockEntities = techHider.getHiddenBlockEntities();
            tileEntities.set(dataWrapper, ((List<?>)tileEntities.get(dataWrapper)).stream().filter(te -> tileEntityVisible(hiddenBlockEntities, te)).collect(Collectors.toList()));

            ByteBuf in = Unpooled.wrappedBuffer(dataField.get(dataWrapper));
            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);
            }
            out.writeBytes(in); // MC appends a 0 byte at the end if there is a full chunk, idk why

            byte[] data = new byte[out.readableBytes()];
            out.readBytes(data);
            dataField.set(dataWrapper, data);

            chunkData.set(packet, dataWrapper);
            return packet;
        };
    }

    public static final Class<?> tileEntity = Reflection.getClass("net.minecraft.network.protocol.game.ClientboundLevelChunkPacketData$BlockEntityInfo");
    protected static final Reflection.Field<TileEntityTypes> entityType = Reflection.getField(tileEntity, TileEntityTypes.class, 0);
    private static final IRegistry<?> tileEntityTypes = Reflection.getField(Core.getVersion() > 18 ? Reflection.getClass("net.minecraft.core.registries.BuiltInRegistries") : IRegistry.class, IRegistry.class, 0, TileEntityTypes.class).get(null);
    private static final Reflection.Method getKey = Reflection.getTypedMethod(IRegistry.class, null, MinecraftKey.class, Object.class);
    private static final Reflection.Method getName = Reflection.getTypedMethod(MinecraftKey.class, null, String.class);
    protected boolean tileEntityVisible(Set<String> hiddenBlockEntities, Object tile) {
        return !hiddenBlockEntities.contains((String) getName.invoke(getKey.invoke(tileEntityTypes, entityType.get(tile))));
    }

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

        boolean singletonPalette = section.getBitsPerBlock() == 0;
        if(singletonPalette) {
            int value = ProtocolUtils.readVarInt(section.getIn());
            ProtocolUtils.writeVarInt(section.getOut(), !section.isSkipSection() && section.getObfuscate().contains(value) ? section.getTarget() : value);
        }else if(section.getBitsPerBlock() < 15) {
            section.processPalette();
        }

        if(section.isSkipSection() || singletonPalette || (!section.blockPrecise() && section.isPaletted())) {
            section.skipDataArray();
            return;
        }

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

        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;

                    switch (section.test(x, y, z)) {
                        case SKIP:
                            break;
                        case CHECK:
                            if(!section.getObfuscate().contains(values.a(pos)))
                                break;
                        case HIDE:
                            values.b(pos, section.getTarget());
                            break;
                        case HIDE_AIR:
                        default:
                            values.b(pos, section.getAir());
                    }
                }
            }
        }

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

    private void biomes(SectionHider section) {
        section.copyBitsPerBlock();
        boolean singletonPalette = section.getBitsPerBlock() == 0;
        if(singletonPalette)
            section.copyVarInt();
        else if(section.getBitsPerBlock() < 6)
            section.skipPalette();

        section.skipDataArray();
    }
}
