/*
 * This file is a part of the SteamWar software.
 *
 * Copyright (C) 2022  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 de.steamwar.Reflection;
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.linkage.Linked;
import lombok.Getter;
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.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.function.BiFunction;

@Linked
public class NoClipCommand extends SWCommand implements Listener {

    public static final Class<?> gameStateChange = Reflection.getClass("net.minecraft.network.protocol.game.ClientboundGameEventPacket");
    private static final Reflection.Field<Float> floatFieldAccessor = Reflection.getField(gameStateChange, float.class, 0);

    private static final Class<?> position = Reflection.getClass("net.minecraft.network.protocol.game.ServerboundMovePlayerPacket$Pos");
    private static final Class<?> positionLook = Reflection.getClass("net.minecraft.network.protocol.game.ServerboundMovePlayerPacket$PosRot");
    private static final Class<?> useItem = Reflection.getClass("net.minecraft.network.protocol.game.ServerboundUseItemOnPacket");
    private static final Class<?> blockDig = Reflection.getClass("net.minecraft.network.protocol.game.ServerboundPlayerActionPacket");
    private static final Class<?> windowClick = Reflection.getClass("net.minecraft.network.protocol.game.ServerboundContainerClickPacket");
    private static final Class<?> setSlotStack = Reflection.getClass("net.minecraft.network.protocol.game.ServerboundSetCreativeModeSlotPacket");

    @Getter
    private static final List<Player> NOCLIPS = new ArrayList<>();
    private static final Map<Player, Long> LAST_TICKS = new HashMap<>();

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

        BiFunction<Player, Object, Object> first = (player, o) -> {
            if (NOCLIPS.contains(player)) {
                if (LAST_TICKS.getOrDefault(player, -1L).equals(TPSUtils.currentTick.get())) return o;
                NMSWrapper.impl.setInternalGameMode(player, GameMode.SPECTATOR);
                LAST_TICKS.put(player, TPSUtils.currentTick.get());
            }
            return o;
        };
        TinyProtocol.instance.addFilter(position, first);
        TinyProtocol.instance.addFilter(positionLook, first);

        BiFunction<Player, Object, Object> second = (player, o) -> {
            if (NOCLIPS.contains(player)) {
                NMSWrapper.impl.setInternalGameMode(player, GameMode.CREATIVE);
                LAST_TICKS.put(player, TPSUtils.currentTick.get());
            }
            return o;
        };
        TinyProtocol.instance.addFilter(useItem, second);
        TinyProtocol.instance.addFilter(blockDig, second);
        TinyProtocol.instance.addFilter(windowClick, second);

        BiFunction<Player, Object, Object> third = (player, o) -> {
            if (NOCLIPS.contains(player)) {
                NMSWrapper.impl.setSlotToItemStack(player, o);
            }
            return o;
        };
        TinyProtocol.instance.addFilter(setSlotStack, third);
    }

    @Register(help = true)
    public void genericCommand(@Validator Player player) {
        if (NOCLIPS.contains(player)) {
            NOCLIPS.remove(player);
            player.setGameMode(GameMode.CREATIVE);
        } else {
            player.setGameMode(GameMode.SPECTATOR);
            NMSWrapper.impl.setPlayerBuildAbilities(player);

            Object gameStateChangeObject = Reflection.newInstance(gameStateChange);
            NMSWrapper.impl.setGameStateChangeReason(gameStateChangeObject);
            floatFieldAccessor.set(gameStateChangeObject, 1F);
            
            NOCLIPS.add(player);
            BauSystem.MESSAGE.send("OTHER_NOCLIP_SLOT_INFO", player);
            TinyProtocol.instance.sendPacket(player, gameStateChangeObject);
            pseudoGameMode(player, GameMode.SPECTATOR);
        }
    }

    @EventHandler
    public void onBauMemberUpdate(BauMemberUpdateEvent event) {
        event.getNewSpectator().forEach(player -> {
            if (NOCLIPS.contains(player)) {
                NOCLIPS.remove(player);
                player.setGameMode(GameMode.CREATIVE);
            }
        });
    }

    @EventHandler(ignoreCancelled = true)
    public void onPlayerGameModeChange(PlayerGameModeChangeEvent event) {
        if (NOCLIPS.contains(event.getPlayer())) {
            event.setCancelled(true);
        }
    }

    @EventHandler(ignoreCancelled = true)
    public void onBlock(BlockCanBuildEvent event) {
        if (NOCLIPS.contains(event.getPlayer())) {
            event.setBuildable(true);
        }
    }

    @EventHandler(ignoreCancelled = true)
    public void onPlayerToggleFlight(PlayerToggleFlightEvent event) {
        if (NOCLIPS.contains(event.getPlayer())) {
            event.setCancelled(true);
            event.getPlayer().setFlying(true);
        }
    }

    @EventHandler(ignoreCancelled = true)
    public void onPlayerTeleport(PlayerTeleportEvent event) {
        if (event.getCause() != PlayerTeleportEvent.TeleportCause.SPECTATE) {
            return;
        }
        if (NOCLIPS.contains(event.getPlayer())) {
            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.impl.playerInfoPacketConstructor(ProtocolWrapper.PlayerInfoAction.GAMEMODE, new GameProfile(player.getUniqueId(), player.getName()), gameMode));
    }
}