package me.km.overrides;

import com.google.common.collect.Lists;
import com.mojang.authlib.GameProfile;
import io.netty.buffer.Unpooled;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.UUID;
import me.hammerle.snuviscript.code.ISnuviScheduler;
import me.km.Server;
import me.km.networking.ModPacketHandler;
import me.km.utils.ReflectionUtils;
import net.minecraft.entity.Entity;
import net.minecraft.entity.EntityType;
import net.minecraft.entity.player.PlayerEntity;
import net.minecraft.entity.player.ServerPlayerEntity;
import net.minecraft.nbt.CompoundNBT;
import net.minecraft.network.NetworkManager;
import net.minecraft.network.PacketBuffer;
import net.minecraft.network.play.ServerPlayNetHandler;
import net.minecraft.network.play.server.SChangeGameStatePacket;
import net.minecraft.network.play.server.SCustomPayloadPlayPacket;
import net.minecraft.network.play.server.SHeldItemChangePacket;
import net.minecraft.network.play.server.SJoinGamePacket;
import net.minecraft.network.play.server.SPlayEntityEffectPacket;
import net.minecraft.network.play.server.SPlayerAbilitiesPacket;
import net.minecraft.network.play.server.SPlayerListItemPacket;
import net.minecraft.network.play.server.SRespawnPacket;
import net.minecraft.network.play.server.SServerDifficultyPacket;
import net.minecraft.network.play.server.SSetExperiencePacket;
import net.minecraft.network.play.server.SSpawnPositionPacket;
import net.minecraft.network.play.server.STagsListPacket;
import net.minecraft.network.play.server.SUpdateRecipesPacket;
import net.minecraft.potion.EffectInstance;
import net.minecraft.server.dedicated.DedicatedPlayerList;
import net.minecraft.server.dedicated.DedicatedServer;
import net.minecraft.server.management.DemoPlayerInteractionManager;
import net.minecraft.server.management.PlayerInteractionManager;
import net.minecraft.server.management.PlayerList;
import net.minecraft.server.management.PlayerProfileCache;
import net.minecraft.util.math.BlockPos;
import net.minecraft.util.math.Vec3d;
import net.minecraft.util.text.TranslationTextComponent;
import net.minecraft.world.IWorld;
import net.minecraft.world.ServerWorld;
import net.minecraft.world.dimension.DimensionType;
import net.minecraft.world.storage.WorldInfo;
import org.apache.logging.log4j.LogManager;

public class ModPlayerList extends DedicatedPlayerList
{
    private final DedicatedServer server;
    private final List<ServerPlayerEntity> players;
    private final Map<UUID, ServerPlayerEntity> uuidToPlayerMap;
    private final ISnuviScheduler scheduler;

    public ModPlayerList(DedicatedServer server, ISnuviScheduler scheduler)
    {
        super(server);
        this.server = server;
        this.players = getPlayers();
        this.uuidToPlayerMap = ReflectionUtils.getFieldValue(Map.class, PlayerList.class, this, "field_177454_f");
        this.scheduler = scheduler;
    }

    @Override
    public void initializeConnectionToPlayer(NetworkManager netManager, ServerPlayerEntity p)
    {
        GameProfile profile = p.getGameProfile();
        PlayerProfileCache cache = this.server.getPlayerProfileCache();
        GameProfile cachedProfile = cache.getProfileByUUID(profile.getId());
        String s = cachedProfile == null ? profile.getName() : cachedProfile.getName();
        cache.addEntry(profile);
        CompoundNBT compoundnbt = this.readPlayerDataFromFile(p);

        //Forge: Make sure the dimension hasn't been deleted, if so stick them in the overworld.
        DimensionType type = p.dimension;
        boolean noSchedule = true;
        if(type == null || !type.isVanilla())
        {
            ServerWorld overWorld = this.server.getWorld(DimensionType.OVERWORLD);
            p.dimension = DimensionType.OVERWORLD;
            if(type != null && this.server.getWorld(type) != null)
            {
                double x = p.posX;
                double y = p.posY;
                double z = p.posZ;
                float yaw = p.rotationYaw;
                float pitch = p.rotationPitch;
                noSchedule = false;
                scheduler.scheduleTask(() -> 
                {
                    ServerWorld ws = server.getWorld(type);
                    if(ws != null)
                    {
                        p.teleport(ws, x, y, z, yaw, pitch);
                    }       
                    net.minecraftforge.fml.hooks.BasicEventHooks.firePlayerLoggedIn(p);
                }, 2);
            }
            WorldInfo wi = overWorld.getWorldInfo();
            p.setPosition(wi.getSpawnX(), wi.getSpawnY(), wi.getSpawnZ());
        }
               
        ServerWorld sw = this.server.getWorld(p.dimension);
        p.setWorld(sw);
        p.interactionManager.setWorld((ServerWorld) p.world);
        String s1 = "local";
        if(netManager.getRemoteAddress() != null)
        {
            s1 = netManager.getRemoteAddress().toString();
        }

        LogManager.getLogger().info("{}[{}] logged in with entity id {} at ({}, {}, {})", p.getName().getString(), s1, p.getEntityId(), p.posX, p.posY, p.posZ);
        WorldInfo wi = sw.getWorldInfo();
        setPlayerGameTypeBasedOnOther(p, null, sw);
        ServerPlayNetHandler spnh = new ServerPlayNetHandler(this.server, netManager, p);
        ModPacketHandler.syncDimensions(p);
        spnh.sendPacket(new SJoinGamePacket(p.getEntityId(), p.interactionManager.getGameType(), wi.isHardcore(), sw.dimension.getType(), this.getMaxPlayers(), wi.getGenerator(), getViewDistance(), sw.getGameRules().getBoolean("reducedDebugInfo")));
        spnh.sendPacket(new SCustomPayloadPlayPacket(SCustomPayloadPlayPacket.BRAND, (new PacketBuffer(Unpooled.buffer())).writeString(this.getServer().getServerModName())));
        spnh.sendPacket(new SServerDifficultyPacket(wi.getDifficulty(), wi.isDifficultyLocked()));
        spnh.sendPacket(new SPlayerAbilitiesPacket(p.abilities));
        spnh.sendPacket(new SHeldItemChangePacket(p.inventory.currentItem));
        spnh.sendPacket(new SUpdateRecipesPacket(this.server.getRecipeManager().getRecipes()));
        spnh.sendPacket(new STagsListPacket(this.server.getNetworkTagManager()));
        this.updatePermissionLevel(p);
        p.getStats().markAllDirty();
        p.getRecipeBook().init(p);
        this.sendScoreboard(sw.getScoreboard(), p);
        this.server.refreshStatusNextTick();

        spnh.setPlayerLocation(p.posX, p.posY, p.posZ, p.rotationYaw, p.rotationPitch);
        this.players.add(p);
        this.uuidToPlayerMap.put(p.getUniqueID(), p);
        this.sendPacketToAllPlayers(new SPlayerListItemPacket(SPlayerListItemPacket.Action.ADD_PLAYER, p));

        for(int i = 0; i < this.players.size(); ++i)
        {
            p.connection.sendPacket(new SPlayerListItemPacket(SPlayerListItemPacket.Action.ADD_PLAYER, this.players.get(i)));
        }

        sw.addNewPlayer(p);
        this.server.getCustomBossEvents().onPlayerLogin(p);
        this.sendWorldInfo(p, sw);
        if(!this.server.getResourcePackUrl().isEmpty())
        {
            p.loadResourcePack(this.server.getResourcePackUrl(), this.server.getResourcePackHash());
        }

        for(EffectInstance effectinstance : p.getActivePotionEffects())
        {
            spnh.sendPacket(new SPlayEntityEffectPacket(p.getEntityId(), effectinstance));
        }

        if(compoundnbt != null && compoundnbt.contains("RootVehicle", 10))
        {
            CompoundNBT compoundnbt1 = compoundnbt.getCompound("RootVehicle");
            final ServerWorld worldf = sw;
            Entity entity1 = EntityType.func_220335_a(compoundnbt1.getCompound("Entity"), sw, (p_217885_1_) ->
            {
                return !worldf.summonEntity(p_217885_1_) ? null : p_217885_1_;
            });
            if(entity1 != null)
            {
                UUID uuid = compoundnbt1.getUniqueId("Attach");
                if(entity1.getUniqueID().equals(uuid))
                {
                    p.startRiding(entity1, true);
                }
                else
                {
                    for(Entity entity : entity1.getRecursivePassengers())
                    {
                        if(entity.getUniqueID().equals(uuid))
                        {
                            p.startRiding(entity, true);
                            break;
                        }
                    }
                }

                if(!p.isPassenger())
                {
                    LogManager.getLogger().warn("Couldn't reattach entity to player");
                    sw.removeEntity(entity1);

                    for(Entity entity2 : entity1.getRecursivePassengers())
                    {
                        sw.removeEntity(entity2);
                    }
                }
            }
        }

        p.addSelfToInternalCraftingInventory();
        if(noSchedule)
        {
            net.minecraftforge.fml.hooks.BasicEventHooks.firePlayerLoggedIn(p);
        }
    }

    @Override
    public ServerPlayerEntity createPlayerForUser(GameProfile profile)
    {
        UUID uuid = PlayerEntity.getUUID(profile);
        List<ServerPlayerEntity> list = Lists.newArrayList();

        for(int i = 0; i < this.players.size(); ++i)
        {
            ServerPlayerEntity serverplayerentity = this.players.get(i);
            if(serverplayerentity.getUniqueID().equals(uuid))
            {
                list.add(serverplayerentity);
            }
        }

        ServerPlayerEntity serverplayerentity2 = this.uuidToPlayerMap.get(profile.getId());
        if(serverplayerentity2 != null && !list.contains(serverplayerentity2))
        {
            list.add(serverplayerentity2);
        }

        for(ServerPlayerEntity serverplayerentity1 : list)
        {
            serverplayerentity1.connection.disconnect(new TranslationTextComponent("multiplayer.disconnect.duplicate_login"));
        }

        PlayerInteractionManager playerinteractionmanager;
        if(this.server.isDemo())
        {
            playerinteractionmanager = new DemoPlayerInteractionManager(this.server.getWorld(DimensionType.OVERWORLD));
        }
        else
        {
            playerinteractionmanager = new PlayerInteractionManager(this.server.getWorld(DimensionType.OVERWORLD));
        }

        return new ModEntityPlayerMP(this.server, this.server.getWorld(DimensionType.OVERWORLD), profile, playerinteractionmanager);
    }
        
    @Override
    public ServerPlayerEntity recreatePlayerEntity(ServerPlayerEntity pIn, DimensionType dim, boolean conqueredEnd)
    {
        Server.scriptEvents.onPlayerPreRespawn(pIn);
        ServerWorld world = server.getWorld(dim);
        if(world == null)
        {
            dim = pIn.getSpawnDimension();
        }
        else if(!world.getDimension().canRespawnHere())
        {
            dim = world.getDimension().getRespawnDimension(pIn);
        }
        if(server.getWorld(dim) == null)
        {
            dim = DimensionType.OVERWORLD;
        }

        this.players.remove(pIn);
        pIn.getServerWorld().removePlayer(pIn, true); // Forge: keep data until copyFrom called
        BlockPos blockpos = pIn.getBedLocation(dim);
        boolean flag = pIn.isSpawnForced(dim);
        pIn.dimension = dim;
        PlayerInteractionManager playerinteractionmanager;
        if(this.server.isDemo())
        {
            playerinteractionmanager = new DemoPlayerInteractionManager(this.server.getWorld(pIn.dimension));
        }
        else
        {
            playerinteractionmanager = new PlayerInteractionManager(this.server.getWorld(pIn.dimension));
        }

        ServerPlayerEntity p = new ModEntityPlayerMP(this.server, this.server.getWorld(pIn.dimension), pIn.getGameProfile(), playerinteractionmanager, (ModEntityPlayerMP) pIn);
        p.connection = pIn.connection;
        p.copyFrom(pIn, conqueredEnd);
        pIn.remove(false); // Forge: clone event had a chance to see old data, now discard it
        p.dimension = dim;
        p.setEntityId(pIn.getEntityId());
        p.setPrimaryHand(pIn.getPrimaryHand());

        for(String s : pIn.getTags())
        {
            p.addTag(s);
        }

        ServerWorld serverworld = this.server.getWorld(pIn.dimension);
        this.setPlayerGameTypeBasedOnOther(p, pIn, serverworld);
        if(blockpos != null)
        {
            Optional<Vec3d> optional = PlayerEntity.func_213822_a(this.server.getWorld(pIn.dimension), blockpos, flag);
            if(optional.isPresent())
            {
                Vec3d vec3d = optional.get();
                p.setLocationAndAngles(vec3d.x, vec3d.y, vec3d.z, 0.0F, 0.0F);
                p.setSpawnPoint(blockpos, flag, dim);
            }
            else
            {
                p.connection.sendPacket(new SChangeGameStatePacket(0, 0.0F));
            }
        }

        while(!serverworld.areCollisionShapesEmpty(p) && p.posY < 256.0D)
        {
            p.setPosition(p.posX, p.posY + 1.0D, p.posZ);
        }

        WorldInfo wi = p.world.getWorldInfo();
        p.connection.sendPacket(new SRespawnPacket(p.dimension, wi.getGenerator(), p.interactionManager.getGameType()));
        BlockPos pos = serverworld.getSpawnPoint();
        p.connection.setPlayerLocation(p.posX, p.posY, p.posZ, p.rotationYaw, p.rotationPitch);
        p.connection.sendPacket(new SSpawnPositionPacket(pos));
        p.connection.sendPacket(new SServerDifficultyPacket(wi.getDifficulty(), wi.isDifficultyLocked()));
        p.connection.sendPacket(new SSetExperiencePacket(p.experience, p.experienceTotal, p.experienceLevel));
        this.sendWorldInfo(p, serverworld);
        this.updatePermissionLevel(p);
        serverworld.addRespawnedPlayer(p);
        this.players.add(p);
        this.uuidToPlayerMap.put(p.getUniqueID(), p);
        p.addSelfToInternalCraftingInventory();
        p.setHealth(p.getHealth());
        net.minecraftforge.fml.hooks.BasicEventHooks.firePlayerRespawnEvent(p, conqueredEnd);
        return p;
    }

    private void setPlayerGameTypeBasedOnOther(ServerPlayerEntity target, ServerPlayerEntity source, IWorld w) 
    {
        ReflectionUtils.setPlayerGameTypeBasedOnOther(this, target, source, w);
    }
}