/*
    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 com.comphenix.tinyprotocol.TinyProtocol;
import de.steamwar.core.Core;
import lombok.Getter;
import org.bukkit.GameMode;
import org.bukkit.Material;
import org.bukkit.entity.Player;

import java.util.*;
import java.util.function.BiFunction;
import java.util.function.Function;
import java.util.function.UnaryOperator;
import java.util.stream.Collectors;

public class TechHider {

    public static final Class<?> blockPosition = Reflection.getClass("net.minecraft.core.BlockPos");
    private static final Class<?> baseBlockPosition = Reflection.getClass("net.minecraft.core.Vec3i");
    public static final Reflection.Field<Integer> blockPositionX = Reflection.getField(baseBlockPosition, int.class, 0);
    public static final Reflection.Field<Integer> blockPositionY = Reflection.getField(baseBlockPosition, int.class, 1);
    public static final Reflection.Field<Integer> blockPositionZ = Reflection.getField(baseBlockPosition, int.class, 2);

    public static final Class<?> iBlockData = Reflection.getClass("net.minecraft.world.level.block.state.BlockState");
    public static final Class<?> block = Reflection.getClass("net.minecraft.world.level.block.Block");
    private static final Reflection.Method getBlockDataByBlock = Reflection.getTypedMethod(block, null, iBlockData);

    public static final Class<?> craftMagicNumbers = Reflection.getClass("org.bukkit.craftbukkit.util.CraftMagicNumbers");
    private static final Reflection.Method getBlockByMaterial = Reflection.getTypedMethod(craftMagicNumbers, "getBlock", block, Material.class);

    private static final Reflection.Method getBlockByBlockData = Reflection.getTypedMethod(iBlockData, null, block);
    private static final Reflection.Method getMaterialByBlock = Reflection.getTypedMethod(craftMagicNumbers, "getMaterial", Material.class, block);
    public boolean iBlockDataHidden(Object iBlockData) {
        return obfuscate.contains((Material) getMaterialByBlock.invoke(null, getBlockByBlockData.invoke(iBlockData)));
    }

    public static final Object AIR = getBlockDataByBlock.invoke(getBlockByMaterial.invoke(null, Material.AIR));
    public static final int AIR_ID = BlockIds.impl.materialToId(Material.AIR);

    private final Map<Class<?>, BiFunction<Player, Object, Object>> techhiders = new HashMap<>();
    @Getter
    private final LocationEvaluator locationEvaluator;
    @Getter
    private final Object obfuscationTarget;
    @Getter
    private final int obfuscationTargetId;
    @Getter
    private final Set<Material> obfuscate;
    @Getter
    private final Set<Integer> obfuscateIds;
    @Getter
    private final Set<String> hiddenBlockEntities;

    public TechHider(LocationEvaluator locationEvaluator, Material obfuscationTarget, Set<Material> obfuscate, Set<String> hiddenBlockEntities) {
        this.locationEvaluator = locationEvaluator;
        this.obfuscate = obfuscate;
        this.obfuscateIds = obfuscate.stream().flatMap(m -> BlockIds.impl.materialToAllIds(m).stream()).collect(Collectors.toSet());
        this.hiddenBlockEntities = hiddenBlockEntities;
        this.obfuscationTarget = getBlockDataByBlock.invoke(getBlockByMaterial.invoke(null, obfuscationTarget));
        this.obfuscationTargetId = BlockIds.impl.materialToId(obfuscationTarget);

        techhiders.put(blockActionPacket, this::blockActionHider);
        techhiders.put(blockChangePacket, this::blockChangeHider);
        techhiders.put(tileEntityDataPacket, this::tileEntityDataHider);
        techhiders.put(multiBlockChangePacket, ProtocolWrapper.impl.multiBlockChangeGenerator(this));
        techhiders.put(ChunkHider.impl.mapChunkPacket(), ChunkHider.impl.chunkHiderGenerator(this));

        if(Core.getVersion() > 12 && Core.getVersion() < 19) {
            Class<?> blockBreakClass = Reflection.getClass("net.minecraft.network.protocol.game.ClientboundBlockDestructionPacket");
            techhiders.put(blockBreakClass, ProtocolWrapper.impl.blockBreakHiderGenerator(blockBreakClass, this));
        }

        if(Core.getVersion() > 8){
            techhiders.put(Reflection.getClass("net.minecraft.network.protocol.game.ServerboundUseItemOnPacket"), (p, packet) -> p.getGameMode() == GameMode.SPECTATOR ? null : packet);
        }
        techhiders.put(Reflection.getClass("net.minecraft.network.protocol.game.ServerboundInteractPacket"), (p, packet) -> p.getGameMode() == GameMode.SPECTATOR ? null : packet);

    }

    public void enable() {
        techhiders.forEach(TinyProtocol.instance::addFilter);
    }

    public void disable() {
        techhiders.forEach(TinyProtocol.instance::removeFilter);
    }

    public static final Class<?> multiBlockChangePacket = Reflection.getClass("net.minecraft.network.protocol.game.ClientboundSectionBlocksUpdatePacket");
    public static final UnaryOperator<Object> multiBlockChangeCloner = ProtocolUtils.shallowCloneGenerator(TechHider.multiBlockChangePacket);

    private static final Class<?> blockChangePacket = Reflection.getClass("net.minecraft.network.protocol.game.ClientboundBlockUpdatePacket");
    private static final Function<Object, Object> blockChangeCloner = ProtocolUtils.shallowCloneGenerator(blockChangePacket);
    private static final Reflection.Field<?> blockChangePosition = Reflection.getField(blockChangePacket, blockPosition, 0);
    private static final Reflection.Field<?> blockChangeBlockData = Reflection.getField(blockChangePacket, iBlockData, 0);
    private Object blockChangeHider(Player p, Object packet) {
        switch (locationEvaluator.checkBlockPos(p, blockChangePosition.get(packet))) {
            case SKIP:
                return packet;
            case CHECK:
                if(!iBlockDataHidden(blockChangeBlockData.get(packet)))
                    return packet;
            case HIDE:
                packet = blockChangeCloner.apply(packet);
                blockChangeBlockData.set(packet, obfuscationTarget);
                return packet;
            case HIDE_AIR:
            default:
                packet = blockChangeCloner.apply(packet);
                blockChangeBlockData.set(packet, AIR);
                return packet;
        }
    }

    private static final Class<?> blockActionPacket = Reflection.getClass("net.minecraft.network.protocol.game.ClientboundBlockEventPacket");
    private static final Reflection.Field<?> blockActionPosition = Reflection.getField(blockActionPacket, blockPosition, 0);
    private Object blockActionHider(Player p, Object packet) {
        if (locationEvaluator.checkBlockPos(p, blockActionPosition.get(packet)) == State.SKIP)
            return packet;
        return null;
    }

    public static final Class<?> tileEntityDataPacket = Reflection.getClass("net.minecraft.network.protocol.game.ClientboundBlockEntityDataPacket");
    private static final Reflection.Field<?> tileEntityDataPosition = Reflection.getField(tileEntityDataPacket, blockPosition, 0);
    private Object tileEntityDataHider(Player p, Object packet) {
        switch (locationEvaluator.checkBlockPos(p, tileEntityDataPosition.get(packet))) {
            case SKIP:
                return packet;
            case CHECK:
                if(ProtocolWrapper.impl.unfilteredTileEntityDataAction(packet))
                    return packet;
            default:
                return null;
        }
    }

    public enum State {
        SKIP,
        CHECK,
        HIDE,
        HIDE_AIR
    }

    public interface LocationEvaluator {
        boolean skipChunk(Player player, int x, int z);
        default boolean skipChunkSection(Player player, int x, int y, int z) {
            return skipChunk(player, x, z);
        }
        default State check(Player player, int x, int y, int z) {
            return skipChunkSection(player, ProtocolUtils.posToChunk(x), ProtocolUtils.posToChunk(y), ProtocolUtils.posToChunk(z)) ? State.SKIP : State.CHECK;
        }

        default State checkBlockPos(Player player, Object pos) {
            return check(player, blockPositionX.get(pos), blockPositionY.get(pos), blockPositionZ.get(pos));
        }

        default boolean blockPrecise(Player player, int x, int y, int z) {
            return false;
        }
    }
}
