/*
 *   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.fightsystem.listener;

import com.comphenix.tinyprotocol.TinyProtocol;
import de.steamwar.Reflection;
import de.steamwar.fightsystem.ArenaMode;
import de.steamwar.fightsystem.FightSystem;
import de.steamwar.fightsystem.events.TeamDeathEvent;
import de.steamwar.fightsystem.events.TeamLeaveEvent;
import de.steamwar.fightsystem.events.TeamSpawnEvent;
import de.steamwar.fightsystem.fight.Fight;
import de.steamwar.fightsystem.fight.FightPlayer;
import de.steamwar.fightsystem.fight.FightTeam;
import de.steamwar.fightsystem.record.GlobalRecorder;
import de.steamwar.fightsystem.states.FightState;
import de.steamwar.fightsystem.states.StateDependent;
import de.steamwar.fightsystem.states.StateDependentListener;
import de.steamwar.fightsystem.states.StateDependentTask;
import de.steamwar.fightsystem.utils.BountifulWrapper;
import de.steamwar.fightsystem.utils.CraftbukkitWrapper;
import de.steamwar.fightsystem.utils.FlatteningWrapper;
import de.steamwar.fightsystem.utils.SWSound;
import de.steamwar.linkage.Linked;
import net.minecraft.network.protocol.game.ServerboundPlayerActionPacket;
import net.minecraft.network.protocol.game.ServerboundUseItemPacket;
import net.minecraft.world.entity.item.PrimedTnt;
import org.bukkit.Bukkit;
import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.entity.Entity;
import org.bukkit.entity.EntityType;
import org.bukkit.entity.LivingEntity;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
import org.bukkit.event.block.BlockPhysicsEvent;
import org.bukkit.event.entity.*;
import org.bukkit.event.inventory.InventoryClickEvent;
import org.bukkit.event.inventory.InventoryType;
import org.bukkit.event.player.*;
import org.bukkit.inventory.ItemStack;

import java.util.Random;
import java.util.function.BiFunction;
import java.util.function.Consumer;
import java.util.function.Predicate;

@Linked
public class Recording implements Listener {

    private static final int AIR = 0;
    private static final Random random = new Random();

    public static ItemStack disarmNull(ItemStack stack) {
        if (stack == null) return new ItemStack(Material.AIR);
        return stack;
    }

    public static boolean isNotSent(LivingEntity p) {
        FightPlayer fp = Fight.getFightPlayer(p);
        return fp == null || !fp.isLiving() || FightState.getFightState() == FightState.SPECTATE;
    }

    public static final Class<?> primedTnt = PrimedTnt.class;

    public static void iterateOverEntities(Predicate<Object> filter, Consumer<Entity> consumer) {
        CraftbukkitWrapper.impl.entityIterator().filter(filter).map(net.minecraft.world.entity.Entity::getBukkitEntity).forEach(consumer);
    }

    public Recording() {
        new StateDependentListener(ArenaMode.AntiReplay, FightState.All, this);
        new StateDependentListener(ArenaMode.AntiReplay, FightState.All, BountifulWrapper.impl.newHandSwapRecorder());
        new StateDependent(ArenaMode.AntiReplay, FightState.Ingame) {
            @Override
            public void enable() {
                Bukkit.getScheduler().runTaskLater(FightSystem.getPlugin(), () -> Fight.teams().forEach(Recording.this::setKitItems), 1);
            }

            @Override
            public void disable() {
                Fight.teams().forEach(Recording.this::despawnTeam);
                despawnTNT();
            }
        }.register();
        new StateDependent(ArenaMode.AntiReplay, FightState.Ingame) {
            private final BiFunction<Player, Object, Object> place = Recording.this::blockPlace;
            private final BiFunction<Player, Object, Object> dig = Recording.this::blockDig;

            @Override
            public void enable() {
                TinyProtocol.instance.addFilter(blockPlacePacket, place);
                TinyProtocol.instance.addFilter(blockDigPacket, dig);
            }

            @Override
            public void disable() {
                TinyProtocol.instance.removeFilter(blockPlacePacket, place);
                TinyProtocol.instance.removeFilter(blockDigPacket, dig);
            }
        }.register();
        new StateDependentTask(ArenaMode.AntiReplay, FightState.All, () -> {
            GlobalRecorder.getInstance().tick();

            if (FightState.getFightState() == FightState.SPECTATE || !GlobalRecorder.getInstance().recording()) return;

            iterateOverEntities(primedTnt::isInstance, this::trackEntity);
        }, 1, 1);
    }

    private void trackEntity(Entity entity) {
        GlobalRecorder.getInstance().entityMoves(entity);
        GlobalRecorder.getInstance().entitySpeed(entity);
    }

    private static final Class<?> blockDigPacket = ServerboundPlayerActionPacket.class;
    private static final Class<?> playerDigType = blockDigPacket.getDeclaredClasses()[0];
    private static final Reflection.Field<?> blockDigType = Reflection.getField(blockDigPacket, playerDigType, 0);
    private static final Object releaseUseItem = playerDigType.getEnumConstants()[5];

    private Object blockDig(Player p, Object packet) {
        if (!isNotSent(p) && blockDigType.get(packet) == releaseUseItem) {
            GlobalRecorder.getInstance().bowSpan(p, false, false);
        }
        return packet;
    }

    public static final Class<?> blockPlacePacket = ServerboundUseItemPacket.class;

    private Object blockPlace(Player p, Object packet) {
        boolean mainHand = BountifulWrapper.impl.mainHand(packet);
        if (!isNotSent(p) && BountifulWrapper.impl.bowInHand(mainHand, p)) {
            GlobalRecorder.getInstance().bowSpan(p, true, !mainHand);
        }
        return packet;
    }

    @EventHandler
    public void onPlayerSpawn(TeamSpawnEvent e) {
        GlobalRecorder.getInstance().playerJoins(e.getFightPlayer().getEntity(), e.getFightPlayer().getUser());
    }

    @EventHandler(priority = EventPriority.HIGH)
    public void onPlayerMove(PlayerMoveEvent e) {
        if (isNotSent(e.getPlayer())) return;

        GlobalRecorder.getInstance().entityMoves(e.getPlayer());
    }

    @EventHandler
    public void onPlayerDeath(TeamDeathEvent e) {
        GlobalRecorder.getInstance().entityDespawns(e.getFightPlayer().getEntity());
    }

    @EventHandler
    public void onPlayerLeave(TeamLeaveEvent e) {
        GlobalRecorder.getInstance().entityDespawns(e.getFightPlayer().getEntity());
    }

    @EventHandler(priority = EventPriority.HIGH, ignoreCancelled = true)
    public void onBlockPhysics(BlockPhysicsEvent e) {
        if (FlatteningWrapper.impl.doRecord(e)) {
            GlobalRecorder.getInstance().blockChange(e.getBlock());
        }
    }

    @EventHandler(priority = EventPriority.HIGH, ignoreCancelled = true)
    public void onSneak(PlayerToggleSneakEvent e) {
        if (isNotSent(e.getPlayer())) return;

        GlobalRecorder.getInstance().playerSneak(e.getPlayer(), e.isSneaking());
    }

    @EventHandler(priority = EventPriority.HIGH, ignoreCancelled = true)
    public void onAnimation(PlayerAnimationEvent e) {
        if (isNotSent(e.getPlayer())) return;

        if (e.getAnimationType() == PlayerAnimationType.ARM_SWING) {
            GlobalRecorder.getInstance().entityAnimation(e.getPlayer(), AIR);
        }
    }

    @EventHandler(priority = EventPriority.HIGH, ignoreCancelled = true)
    public void onEntityDamage(EntityDamageEvent e) {
        if (!e.getEntityType().isAlive()) return;

        LivingEntity p = (LivingEntity) e.getEntity();
        if (isNotSent(p)) return;

        GlobalRecorder.getInstance().damageAnimation(p);

        if (e.getCause() == EntityDamageEvent.DamageCause.FIRE_TICK || e.getCause() == EntityDamageEvent.DamageCause.FIRE) {
            GlobalRecorder.getInstance().setOnFire(p, false);
        }
    }

    @EventHandler(priority = EventPriority.HIGH, ignoreCancelled = true)
    public void onEntityCombust(EntityCombustEvent e) {
        if (!e.getEntityType().isAlive()) return;

        LivingEntity p = (LivingEntity) e.getEntity();
        if (isNotSent(p)) return;

        GlobalRecorder.getInstance().setOnFire(p, false);
    }

    @EventHandler(priority = EventPriority.HIGH, ignoreCancelled = true)
    public void onTNTSpawn(EntitySpawnEvent e) {
        if (e.getEntityType() != EntityType.TNT) return;

        GlobalRecorder.getInstance().tntSpawn(e.getEntity());
    }

    @EventHandler(priority = EventPriority.HIGH, ignoreCancelled = true)
    public void onExplosion(EntityExplodeEvent e) {
        if (e.getEntityType() != EntityType.TNT) return;

        Location loc = e.getLocation();
        GlobalRecorder.getInstance().entityDespawns(e.getEntity());
        GlobalRecorder.getInstance().particle(loc.getX(), loc.getY(), loc.getZ(), "EXPLOSION_HUGE");
        GlobalRecorder.getInstance().sound(loc.getBlockX(), loc.getBlockY(), loc.getBlockZ(), SWSound.ENTITY_GENERIC_EXPLODE, "BLOCKS", 4.0F, (1.0F + (random.nextFloat() - random.nextFloat()) * 0.2F) * 0.7F);
    }

    @EventHandler(priority = EventPriority.HIGH, ignoreCancelled = true)
    public void onItem(PlayerItemHeldEvent e) {
        if (isNotSent(e.getPlayer())) return;

        GlobalRecorder.getInstance().item(e.getPlayer(), disarmNull(e.getPlayer().getInventory().getItem(e.getNewSlot())), "MAINHAND");
    }

    @EventHandler(priority = EventPriority.HIGH, ignoreCancelled = true)
    public void onProjectileSpawn(ProjectileLaunchEvent e) {
        if (e.getEntityType() == EntityType.FIREBALL) {
            GlobalRecorder.getInstance().fireballSpawn(e.getEntity());
        } else if (e.getEntityType() == EntityType.ARROW) {
            GlobalRecorder.getInstance().arrowSpawn(e.getEntity());
        }
    }

    @EventHandler(priority = EventPriority.HIGH, ignoreCancelled = true)
    public void onInventoryClick(InventoryClickEvent e) {
        Player player = (Player) e.getWhoClicked();
        if (isNotSent(player)) return;

        if (e.getSlotType() != InventoryType.SlotType.ARMOR) return;

        switch (e.getSlot()) {
            case 103:
                GlobalRecorder.getInstance().item(player, disarmNull(e.getCurrentItem()), "HEAD");
                break;
            case 102:
                GlobalRecorder.getInstance().item(player, disarmNull(e.getCurrentItem()), "CHEST");
                break;
            case 101:
                GlobalRecorder.getInstance().item(player, disarmNull(e.getCurrentItem()), "LEGS");
                break;
            case 100:
            default:
                GlobalRecorder.getInstance().item(player, disarmNull(e.getCurrentItem()), "FEET");
        }
    }

    private void setKitItems(FightTeam team) {
        if (FightState.getFightState() != FightState.PRE_RUNNING) return;

        for (FightPlayer fp : team.getPlayers()) {
            if (!fp.isLiving()) continue;

            fp.ifPlayer(player -> {
                BountifulWrapper.impl.recordHandItems(player);
                GlobalRecorder.getInstance().item(player, disarmNull(player.getInventory().getHelmet()), "HEAD");
                GlobalRecorder.getInstance().item(player, disarmNull(player.getInventory().getChestplate()), "CHEST");
                GlobalRecorder.getInstance().item(player, disarmNull(player.getInventory().getLeggings()), "LEGS");
                GlobalRecorder.getInstance().item(player, disarmNull(player.getInventory().getBoots()), "FEET");
            });
        }
    }

    private void despawnTeam(FightTeam team) {
        for (FightPlayer player : team.getPlayers()) {
            if (player.isLiving()) {
                GlobalRecorder.getInstance().entityDespawns(player.getEntity());
            }
        }
    }

    private void despawnTNT() {
        iterateOverEntities(primedTnt::isInstance, GlobalRecorder.getInstance()::entityDespawns);
    }
}
