/*
 *   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;

import de.steamwar.Reflection;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
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 ChunkHider9 extends ChunkHider8 {

    private static final UnaryOperator<Object> mapChunkCloner = ProtocolUtils.shallowCloneGenerator(mapChunkPacket);

    private static final Reflection.Field<Integer> mapChunkX = Reflection.getField(mapChunkPacket, int.class, 0);
    private static final Reflection.Field<Integer> mapChunkZ = Reflection.getField(mapChunkPacket, int.class, 1);
    private static final Reflection.Field<Integer> mapChunkBitMask = Reflection.getField(mapChunkPacket, int.class, 2);
    private static final Reflection.Field<List> mapChunkBlockEntities = Reflection.getField(mapChunkPacket, List.class, 0);
    private static final Reflection.Field<byte[]> mapChunkData = Reflection.getField(mapChunkPacket, byte[].class, 0);

    private static final Class<?> nbtTagCompound = Reflection.getClass("net.minecraft.nbt.CompoundTag");
    private static final Reflection.Method nbtTagGetString = Reflection.getTypedMethod(nbtTagCompound, null, String.class, String.class);

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

            packet = mapChunkCloner.apply(packet);
            Set<String> hiddenBlockEntities = techHider.getHiddenBlockEntities();
            mapChunkBlockEntities.set(packet, ((List<?>)mapChunkBlockEntities.get(packet)).stream().filter(
                    nbttag -> !hiddenBlockEntities.contains((String) nbtTagGetString.invoke(nbttag, "id"))
            ).collect(Collectors.toList()));

            int primaryBitMask = mapChunkBitMask.get(packet);
            ByteBuf in = Unpooled.wrappedBuffer(mapChunkData.get(packet));
            ByteBuf out = Unpooled.buffer(in.readableBytes() + 64);
            for(int chunkY = 0; chunkY < p.getWorld().getMaxHeight()/16; chunkY++) {
                if(((1 << chunkY) & primaryBitMask) == 0)
                    continue;

                SectionHider section = new SectionHider(p, techHider, in, out, chunkX, chunkY, chunkZ);
                dataHider(section);
            }
            byte[] data = new byte[out.readableBytes()];
            out.readBytes(data);
            mapChunkData.set(packet, data);

            return packet;
        };
    }

    protected void dataHider(SectionHider section) {
        section.copyBitsPerBlock();
        section.processPalette();

        if(section.isSkipSection() || (!section.blockPrecise() && section.isPaletted())) { //section.getBitsPerBlock() < 9
            section.skipDataArray();
        } else {
            processDataArray(section);
        }

        section.getOut().writeBytes(section.getIn(), 4096); //Skylight (Not in Nether/End!!!) 2048 + Blocklight 2048
    }

    protected void processDataArray(SectionHider section) {
        VariableValueArray values = new VariableValueArray(section.getBitsPerBlock(), 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.get(pos)))
                                break;
                        case HIDE:
                            values.set(pos, section.getTarget());
                            break;
                        case HIDE_AIR:
                        default:
                            values.set(pos, section.getAir());
                    }
                }
            }
        }

        section.writeDataArray(values.backing);
    }

    private static class VariableValueArray {
        private final long[] backing;
        private final int bitsPerValue;
        private final long valueMask;

        public VariableValueArray(int bitsPerEntry, long[] dataArray) {
            this.bitsPerValue = bitsPerEntry;
            this.valueMask = (1L << this.bitsPerValue) - 1;
            this.backing = dataArray;
        }

        public int get(int index) {
            index *= bitsPerValue;
            int i0 = index >> 6;
            int i1 = index & 0x3f;

            long value = backing[i0] >>> i1;

            // The value is divided over two long values
            if (i1 + bitsPerValue > 64)
                value |= backing[++i0] << 64 - i1;

            return (int) (value & valueMask);
        }

        public void set(int index, int value) {
            index *= bitsPerValue;
            int i0 = index >> 6;
            int i1 = index & 0x3f;

            backing[i0] = backing[i0] & ~(valueMask << i1) | (value & valueMask) << i1;
            int i2 = i1 + bitsPerValue;
            // The value is divided over two long values
            if (i2 > 64) {
                i0++;
                backing[i0] = backing[i0] & -(1L << i2 - 64) | value >> 64 - i1;
            }
        }
    }
}
