/*
 *   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.bausystem.features.util;

import com.comphenix.tinyprotocol.TinyProtocol;
import com.mojang.authlib.GameProfile;
import de.steamwar.bausystem.BauSystem;
import de.steamwar.bausystem.features.tpslimit.TPSUtils;
import de.steamwar.bausystem.utils.BauMemberUpdateEvent;
import de.steamwar.bausystem.utils.NMSWrapper;
import de.steamwar.command.SWCommand;
import de.steamwar.core.ProtocolWrapper;
import de.steamwar.core.SWPlayer;
import de.steamwar.linkage.Linked;
import net.minecraft.network.protocol.game.*;
import org.bukkit.Bukkit;
import org.bukkit.GameMode;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.block.BlockCanBuildEvent;
import org.bukkit.event.player.PlayerGameModeChangeEvent;
import org.bukkit.event.player.PlayerTeleportEvent;
import org.bukkit.event.player.PlayerToggleFlightEvent;

import java.util.function.BiFunction;

@Linked
public class NoClipCommand extends SWCommand implements Listener {

    public static class NoClipData implements SWPlayer.Component {
        private long lastTick = -1;

        @Override
        public void onUnmount(SWPlayer player) {
            player.setGameMode(GameMode.CREATIVE);
        }
    }

    public NoClipCommand() {
        super("noclip", "nc");

        BiFunction<Player, Object, Object> first = (player, o) -> {
            NoClipData noClipData = SWPlayer.of(player).getComponent(NoClipData.class).orElse(null);
            if (noClipData == null) return o;
            if (noClipData.lastTick == TPSUtils.currentTick.get()) return o;
            NMSWrapper.impl.setInternalGameMode(player, GameMode.SPECTATOR);
            noClipData.lastTick = TPSUtils.currentTick.get();
            return o;
        };
        TinyProtocol.instance.addFilter(ServerboundMovePlayerPacket.Pos.class, first);
        TinyProtocol.instance.addFilter(ServerboundMovePlayerPacket.PosRot.class, first);

        BiFunction<Player, Object, Object> second = (player, o) -> {
            NoClipData noClipData = SWPlayer.of(player).getComponent(NoClipData.class).orElse(null);
            if (noClipData == null) return o;
            NMSWrapper.impl.setInternalGameMode(player, GameMode.CREATIVE);
            noClipData.lastTick = TPSUtils.currentTick.get();
            return o;
        };
        TinyProtocol.instance.addFilter(ServerboundUseItemOnPacket.class, second);
        TinyProtocol.instance.addFilter(ServerboundPlayerActionPacket.class, second);
        TinyProtocol.instance.addFilter(ServerboundContainerClickPacket.class, second);

        BiFunction<Player, Object, Object> third = (player, o) -> {
            if (SWPlayer.of(player).hasComponent(NoClipData.class)) {
                NMSWrapper.impl.setSlotToItemStack(player, o);
            }
            return o;
        };
        TinyProtocol.instance.addFilter(ServerboundSetCreativeModeSlotPacket.class, third);
    }

    @Register(help = true)
    public void genericCommand(@Validator Player player) {
        SWPlayer swPlayer = SWPlayer.of(player);
        if (swPlayer.hasComponent(NoClipData.class)) {
            swPlayer.removeComponent(NoClipData.class);
        } else {
            player.setGameMode(GameMode.SPECTATOR);
            NMSWrapper.impl.setPlayerBuildAbilities(player);

            swPlayer.setComponent(new NoClipData());
            BauSystem.MESSAGE.send("OTHER_NOCLIP_SLOT_INFO", player);
            TinyProtocol.instance.sendPacket(player, new ClientboundGameEventPacket(ClientboundGameEventPacket.CHANGE_GAME_MODE, 1F));
            pseudoGameMode(player, GameMode.SPECTATOR);
        }
    }

    @EventHandler
    public void onBauMemberUpdate(BauMemberUpdateEvent event) {
        event.getNewSpectator().forEach(player -> {
            SWPlayer.of(player).removeComponent(NoClipData.class);
        });
    }

    @EventHandler(ignoreCancelled = true)
    public void onPlayerGameModeChange(PlayerGameModeChangeEvent event) {
        if (SWPlayer.of(event.getPlayer()).hasComponent(NoClipData.class)) {
            event.setCancelled(true);
        }
    }

    @EventHandler(ignoreCancelled = true)
    public void onBlock(BlockCanBuildEvent event) {
        if (SWPlayer.of(event.getPlayer()).hasComponent(NoClipData.class)) {
            event.setBuildable(true);
        }
    }

    @EventHandler(ignoreCancelled = true)
    public void onPlayerToggleFlight(PlayerToggleFlightEvent event) {
        if (SWPlayer.of(event.getPlayer()).hasComponent(NoClipData.class)) {
            event.setCancelled(true);
            event.getPlayer().setFlying(true);
        }
    }

    @EventHandler(ignoreCancelled = true)
    public void onPlayerTeleport(PlayerTeleportEvent event) {
        if (event.getCause() != PlayerTeleportEvent.TeleportCause.SPECTATE) {
            return;
        }
        if (SWPlayer.of(event.getPlayer()).hasComponent(NoClipData.class)) {
            event.setCancelled(true);
            Bukkit.getScheduler().runTaskLater(BauSystem.getInstance(), () -> {
                event.getPlayer().setSpectatorTarget(null);
            }, 1L);
        }
    }

    private static void pseudoGameMode(Player player, GameMode gameMode) {
        TinyProtocol.instance.sendPacket(player, ProtocolWrapper.playerInfoPacketConstructor(ProtocolWrapper.PlayerInfoAction.GAMEMODE, new GameProfile(player.getUniqueId(), player.getName()), gameMode));
    }
}