/*
 *    This file is a part of the SteamWar software.
 *
 *    Copyright (C) 2023  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.core.events;

import de.steamwar.Reflection;
import com.comphenix.tinyprotocol.TinyProtocol;
import de.steamwar.core.Core;
import de.steamwar.sql.SWException;
import de.steamwar.techhider.ProtocolUtils;
import de.steamwar.techhider.TechHider;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerQuitEvent;

import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Function;

public class AntiNocom implements Listener {

    private final Map<Player, Integer> flags = new ConcurrentHashMap<>();

    public AntiNocom() {
        Bukkit.getServer().getPluginManager().registerEvents(this, Core.getInstance());
        TinyProtocol.instance.addFilter(blockDig, this::onDig);

        if(Core.getVersion() > 8) {
            registerUseItem();
        }
    }

    @EventHandler
    public void onQuit(PlayerQuitEvent e) {
        flags.remove(e.getPlayer());
    }

    private void registerUseItem() {
        Class<?> useItem = Reflection.getClass("net.minecraft.network.protocol.game.ServerboundUseItemOnPacket");

        Function<Object, Object> getPosition;
        if(Core.getVersion() > 12) {
            Class<?> movingObjectPositionBlock = Reflection.getClass("net.minecraft.world.phys.BlockHitResult");
            Reflection.Field<?> useItemPosition = Reflection.getField(useItem, movingObjectPositionBlock, 0);
            Reflection.Field<?> movingBlockPosition = Reflection.getField(movingObjectPositionBlock, TechHider.blockPosition, 0);

            getPosition = (packet) -> movingBlockPosition.get(useItemPosition.get(packet));
        } else {
            getPosition = Reflection.getField(useItem, TechHider.blockPosition, 0)::get;
        }

        TinyProtocol.instance.addFilter(useItem, (player, packet) -> {
            Object pos = getPosition.apply(packet);
            return isValid(player, "UseItem", TechHider.blockPositionX.get(pos), TechHider.blockPositionZ.get(pos)) ? packet : null;
        });
    }

    private static final Class<?> blockDig = Reflection.getClass("net.minecraft.network.protocol.game.ServerboundPlayerActionPacket");
    private static final Reflection.Field<?> digPosition = Reflection.getField(blockDig, TechHider.blockPosition, 0);
    private Object onDig(Player player, Object packet) {
        Object pos = digPosition.get(packet);
        return isValid(player, "Dig", TechHider.blockPositionX.get(pos), TechHider.blockPositionZ.get(pos)) ? packet : null;
    }

    private boolean isValid(Player player, String type, int x, int z) {
        if((x == 0 && z == 0) || player.getWorld().isChunkLoaded(ProtocolUtils.posToChunk(x), ProtocolUtils.posToChunk(z)))
            return true;

        int amount = flags.compute(player, (p, a) -> a == null ? 1 : a+1);
        if(amount % 8 == 0) {  // Only after 8 and every 8 to reduce false flags and spam
            if(amount == 8)  // Schedule player kick only once
                Bukkit.getScheduler().runTask(Core.getInstance(), () -> player.kickPlayer(null));

            SWException.log(player.getName() + " kicked for potential NoCom-DOS attack", x + " " + z + " " + type + " " + amount);
        }
        return false;
    }
}
