package com.bringholm.nametagchanger;

import com.bringholm.reflectutil.v1_1_1.ReflectUtil;
import com.comphenix.protocol.PacketType;
import com.comphenix.protocol.ProtocolLibrary;
import com.comphenix.protocol.events.PacketAdapter;
import com.comphenix.protocol.events.PacketContainer;
import com.comphenix.protocol.events.PacketEvent;
import com.comphenix.protocol.wrappers.*;
import com.google.common.collect.Lists;
import org.bukkit.Bukkit;
import org.bukkit.Material;
import org.bukkit.entity.Player;
import org.bukkit.inventory.ItemStack;
import org.bukkit.plugin.Plugin;

import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.*;
import java.util.logging.Level;

/**
 * The packet handler implementation using ProtocolLib
 * @author AlvinB
 */
public class ProtocolLibPacketHandler extends PacketAdapter implements IPacketHandler {

    private static final Class<?> ENTITY_PLAYER = ReflectUtil.getNMSClass("EntityPlayer").getOrThrow();
    private static final Method GET_HANDLE = ReflectUtil.getMethod(ReflectUtil.getCBClass("entity.CraftPlayer").getOrThrow(), "getHandle").getOrThrow();
    private static final Field PING = ReflectUtil.getField(ENTITY_PLAYER, "ping").getOrThrow();

    private static final int CREATE_SCOREBOARD_TEAM_MODE = 0;
    private static final int JOIN_SCOREBOARD_TEAM_MODE = 3;
    private static final int LEAVE_SCOREBOARD_TEAM_MODE = 4;

    ProtocolLibPacketHandler(Plugin plugin) {
        super(plugin, PacketType.Play.Server.PLAYER_INFO, PacketType.Play.Server.SCOREBOARD_TEAM);
        ProtocolLibrary.getProtocolManager().addPacketListener(this);
    }

    @Override
    public void onPacketSending(PacketEvent e) {
        if (NameTagChanger.INSTANCE.sendingPackets) {
            return;
        }
        if (e.getPacketType() == PacketType.Play.Server.PLAYER_INFO) {
            List<PlayerInfoData> list = Lists.newArrayList();
            boolean modified = false;
            for (PlayerInfoData infoData : e.getPacket().getPlayerInfoDataLists().read(0)) {
                if (NameTagChanger.INSTANCE.gameProfiles.containsKey(infoData.getProfile().getUUID())) {
                    UUID uuid = infoData.getProfile().getUUID();
                    Player player = Bukkit.getPlayer(uuid);
                    WrappedChatComponent displayName = infoData.getDisplayName() == null ? WrappedChatComponent.fromText(player == null ? infoData.getProfile().getName() : player.getPlayerListName()) : infoData.getDisplayName();
                    WrappedGameProfile gameProfile = getProtocolLibProfileWrapper(NameTagChanger.INSTANCE.gameProfiles.get(uuid));
                    PlayerInfoData newInfoData = new PlayerInfoData(gameProfile, infoData.getLatency(), infoData.getGameMode(), displayName);
                    list.add(newInfoData);
                    modified = true;
                } else {
                    list.add(infoData);
                }
            }
            if (modified) {
                e.getPacket().getPlayerInfoDataLists().write(0, list);
            }
        } else {
            int mode = e.getPacket().getIntegers().read(1);
            if (mode == CREATE_SCOREBOARD_TEAM_MODE || mode == LEAVE_SCOREBOARD_TEAM_MODE || mode == JOIN_SCOREBOARD_TEAM_MODE) {
                @SuppressWarnings("unchecked") Collection<String> entriesToAdd = (Collection<String>) e.getPacket().getSpecificModifier(Collection.class).read(0);
                Map<UUID, String> changedPlayerNames = NameTagChanger.INSTANCE.getChangedPlayers();
                //noinspection Duplicates
                for (String entry : entriesToAdd) {
                    for (UUID uuid : changedPlayerNames.keySet()) {
                        Player changedPlayer = Bukkit.getPlayer(uuid);
                        if (changedPlayer != null && changedPlayer.getName().equals(entry)) {
                            entriesToAdd.remove(entry);
                            entriesToAdd.add(changedPlayerNames.get(uuid));
                            break;
                        }
                    }
                }
            }
        }
    }

    @Override
    public void sendTabListRemovePacket(Player playerToRemove, Player seer) {
        PacketContainer packet = ProtocolLibrary.getProtocolManager().createPacket(PacketType.Play.Server.PLAYER_INFO);
        packet.getPlayerInfoAction().write(0, EnumWrappers.PlayerInfoAction.REMOVE_PLAYER);
        PlayerInfoData playerInfoData = new PlayerInfoData(WrappedGameProfile.fromPlayer(playerToRemove), 0, EnumWrappers.NativeGameMode.NOT_SET, null);
        packet.getPlayerInfoDataLists().write(0, Collections.singletonList(playerInfoData));
        try {
            ProtocolLibrary.getProtocolManager().sendServerPacket(seer, packet);
        } catch (InvocationTargetException e) {
            logMessage(Level.SEVERE, "Failed to send tab list remove packet!", e);
        }
    }

    @Override
    public void sendTabListAddPacket(Player playerToAdd, GameProfileWrapper newProfile, Player seer) {
        PacketContainer packet = ProtocolLibrary.getProtocolManager().createPacket(PacketType.Play.Server.PLAYER_INFO);
        int ping = (int) ReflectUtil.getFieldValue(ReflectUtil.invokeMethod(playerToAdd, GET_HANDLE).getOrThrow(), PING).getOrThrow();
        packet.getPlayerInfoAction().write(0, EnumWrappers.PlayerInfoAction.ADD_PLAYER);
        PlayerInfoData playerInfoData = new PlayerInfoData(getProtocolLibProfileWrapper(newProfile), ping, EnumWrappers.NativeGameMode.fromBukkit(playerToAdd.getGameMode()), WrappedChatComponent.fromText(playerToAdd.getPlayerListName()));
        packet.getPlayerInfoDataLists().write(0, Collections.singletonList(playerInfoData));
        try {
            ProtocolLibrary.getProtocolManager().sendServerPacket(seer, packet);
        } catch (InvocationTargetException e) {
            logMessage(Level.SEVERE, "Failed to send tab list add packet!", e);
        }
    }

    @Override
    public void sendEntityDestroyPacket(Player playerToDestroy, Player seer) {
        PacketContainer packet = ProtocolLibrary.getProtocolManager().createPacket(PacketType.Play.Server.ENTITY_DESTROY);
        packet.getIntegerArrays().write(0, new int[] {playerToDestroy.getEntityId()});
        try {
            ProtocolLibrary.getProtocolManager().sendServerPacket(seer, packet);
        } catch (InvocationTargetException e) {
            logMessage(Level.SEVERE, "Failed to send entity destroy packet!", e);
        }
    }

    @Override
    public void sendNamedEntitySpawnPacket(Player playerToSpawn, Player seer) {
        PacketContainer packet = ProtocolLibrary.getProtocolManager().createPacket(PacketType.Play.Server.NAMED_ENTITY_SPAWN);
        packet.getIntegers().write(0, playerToSpawn.getEntityId());
        packet.getUUIDs().write(0, playerToSpawn.getUniqueId());
        if (ReflectUtil.isVersionHigherThan(1, 8, 8)) {
            packet.getDoubles().write(0, playerToSpawn.getLocation().getX());
            packet.getDoubles().write(1, playerToSpawn.getLocation().getY());
            packet.getDoubles().write(2, playerToSpawn.getLocation().getZ());
        } else {
            packet.getIntegers().write(0, (int) Math.floor(playerToSpawn.getLocation().getX() * 32D));
            packet.getIntegers().write(1, (int) Math.floor(playerToSpawn.getLocation().getY() * 32D));
            packet.getIntegers().write(2, (int) Math.floor(playerToSpawn.getLocation().getZ() * 32D));
        }
        packet.getBytes().write(0, (byte) (playerToSpawn.getLocation().getYaw() * 256F / 360F));
        packet.getBytes().write(1, (byte) (playerToSpawn.getLocation().getPitch() * 256F / 360F));
        packet.getDataWatcherModifier().write(0, WrappedDataWatcher.getEntityWatcher(playerToSpawn));
        try {
            ProtocolLibrary.getProtocolManager().sendServerPacket(seer, packet);
        } catch (InvocationTargetException e) {
            logMessage(Level.SEVERE, "Failed to send named entity spawn packet!", e);
        }
    }

    @Override
    public void sendEntityEquipmentPacket(Player playerToSpawn, Player seer) {
        int entityID = playerToSpawn.getEntityId();
        // ProtocolLib converts some ItemStacks with Material.AIR to null, causing exceptions
        if (playerToSpawn.getInventory().getItemInMainHand() != null && playerToSpawn.getInventory().getItemInMainHand().getType() != Material.AIR) {
            doEquipmentPacketSend(entityID, EnumWrappers.ItemSlot.MAINHAND, playerToSpawn.getInventory().getItemInMainHand(), seer);
        }
        if (playerToSpawn.getInventory().getItemInOffHand() != null && playerToSpawn.getInventory().getItemInOffHand().getType() != Material.AIR) {
            doEquipmentPacketSend(entityID, EnumWrappers.ItemSlot.OFFHAND, playerToSpawn.getInventory().getItemInOffHand(), seer);
        }
        if (playerToSpawn.getInventory().getBoots() != null && playerToSpawn.getInventory().getBoots().getType() != Material.AIR) {
            doEquipmentPacketSend(entityID, EnumWrappers.ItemSlot.FEET, playerToSpawn.getInventory().getBoots(), seer);
        }
        if (playerToSpawn.getInventory().getLeggings() != null && playerToSpawn.getInventory().getLeggings().getType() != Material.AIR) {
            doEquipmentPacketSend(entityID, EnumWrappers.ItemSlot.LEGS, playerToSpawn.getInventory().getLeggings(), seer);
        }
        if (playerToSpawn.getInventory().getChestplate() != null && playerToSpawn.getInventory().getChestplate().getType() != Material.AIR) {
            doEquipmentPacketSend(entityID, EnumWrappers.ItemSlot.CHEST, playerToSpawn.getInventory().getChestplate(), seer);
        }
        if (playerToSpawn.getInventory().getHelmet() != null && playerToSpawn.getInventory().getHelmet().getType() != Material.AIR) {
            doEquipmentPacketSend(entityID, EnumWrappers.ItemSlot.HEAD, playerToSpawn.getInventory().getHelmet(), seer);
        }
    }

    private void doEquipmentPacketSend(int entityID, EnumWrappers.ItemSlot slot, ItemStack itemStack, Player recipient) {
        PacketContainer packet = ProtocolLibrary.getProtocolManager().createPacket(PacketType.Play.Server.ENTITY_EQUIPMENT);
        packet.getIntegers().write(0, entityID);
        packet.getItemSlots().write(0, slot);
        packet.getItemModifier().write(0, itemStack);
        try {
            ProtocolLibrary.getProtocolManager().sendServerPacket(recipient, packet);
        } catch (InvocationTargetException e) {
            logMessage(Level.SEVERE, "Failed to send equipment packet!", e);
        }
    }

    @Override
    public void sendScoreboardRemovePacket(String playerToRemove, Player seer, String team) {
        try {
            ProtocolLibrary.getProtocolManager().sendServerPacket(seer, getScoreboardPacket(team, playerToRemove, LEAVE_SCOREBOARD_TEAM_MODE));
        } catch (InvocationTargetException e) {
            logMessage(Level.SEVERE, "Failed to send scoreboard remove packet!", e);
        }
    }

    @Override
    public void sendScoreboardAddPacket(String playerToAdd, Player seer, String team) {
        try {
            ProtocolLibrary.getProtocolManager().sendServerPacket(seer, getScoreboardPacket(team, playerToAdd, JOIN_SCOREBOARD_TEAM_MODE));
        } catch (InvocationTargetException e) {
            logMessage(Level.SEVERE, "Failed to send scoreboard add packet!", e);
        }
    }

    @SuppressWarnings("unchecked")
    private PacketContainer getScoreboardPacket(String team, String entryToAdd, int mode) {
        PacketContainer packet = ProtocolLibrary.getProtocolManager().createPacket(PacketType.Play.Server.SCOREBOARD_TEAM);
        packet.getStrings().write(0, team);
        packet.getIntegers().write(1, mode);
        ((Collection<String>) packet.getSpecificModifier(Collection.class).read(0)).add(entryToAdd);
        return packet;
    }

    @Override
    public GameProfileWrapper getDefaultPlayerProfile(Player player) {
        WrappedGameProfile wrappedGameProfile = WrappedGameProfile.fromPlayer(player);
        GameProfileWrapper wrapper = new GameProfileWrapper(wrappedGameProfile.getUUID(), wrappedGameProfile.getName());
        for (Map.Entry<String, Collection<WrappedSignedProperty>> entry : wrappedGameProfile.getProperties().asMap().entrySet()) {
            for (WrappedSignedProperty wrappedSignedProperty : entry.getValue()) {
                wrapper.getProperties().put(entry.getKey(), new GameProfileWrapper.PropertyWrapper(wrappedSignedProperty.getName(), wrappedSignedProperty.getValue(), wrappedSignedProperty.getSignature()));
            }
        }
        return wrapper;
    }

    private static WrappedGameProfile getProtocolLibProfileWrapper(GameProfileWrapper wrapper) {
        WrappedGameProfile wrappedGameProfile = new WrappedGameProfile(wrapper.getUUID(), wrapper.getName());
        for (Map.Entry<String, Collection<GameProfileWrapper.PropertyWrapper>> entry : wrapper.getProperties().asMap().entrySet()) {
            for (GameProfileWrapper.PropertyWrapper propertyWrapper : entry.getValue()) {
                wrappedGameProfile.getProperties().put(entry.getKey(), new WrappedSignedProperty(propertyWrapper.getName(), propertyWrapper.getValue(), propertyWrapper.getSignature()));
            }
        }
        return wrappedGameProfile;
    }

    @Override
    public void shutdown() {
        ProtocolLibrary.getProtocolManager().removePacketListener(this);
    }

    private void logMessage(Level level, String message, Exception e) {
        if (level == Level.SEVERE) {
            System.err.println("[NameTagChanger] " + message);
        } else {
            NameTagChanger.INSTANCE.printMessage(message);
        }
        if (e != null) {
            e.printStackTrace();
        }
    }
}