package net.md_5.bungee;

import com.google.common.base.Objects;
import com.google.common.base.Preconditions;
import io.netty.bootstrap.Bootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelOption;
import java.net.InetSocketAddress;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.Queue;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.Synchronized;
import lombok.ToString;
import net.md_5.bungee.api.Callback;
import net.md_5.bungee.api.CommandSender;
import net.md_5.bungee.api.ProxyServer;
import net.md_5.bungee.api.ServerPing;
import net.md_5.bungee.api.config.ServerInfo;
import net.md_5.bungee.api.connection.ProxiedPlayer;
import net.md_5.bungee.api.connection.Server;
import net.md_5.bungee.connection.PingHandler;
import net.md_5.bungee.netty.HandlerBoss;
import net.md_5.bungee.netty.PipelineUtils;
import net.md_5.bungee.protocol.DefinedPacket;
import net.md_5.bungee.protocol.packet.PluginMessage;

@RequiredArgsConstructor
@ToString(of =
{
    "name", "address", "restricted"
})
public class BungeeServerInfo implements ServerInfo
{

    @Getter
    private final String name;
    @Getter
    private final InetSocketAddress address;
    private final Collection<ProxiedPlayer> players = new ArrayList<>();
    @Getter
    private final String motd;
    @Getter
    private final boolean restricted;
    @Getter
    private final Queue<DefinedPacket> packetQueue = new LinkedList<>();

    @Synchronized("players")
    public void addPlayer(ProxiedPlayer player)
    {
        players.add( player );
    }

    @Synchronized("players")
    public void removePlayer(ProxiedPlayer player)
    {
        players.remove( player );
    }

    @Synchronized("players")
    @Override
    public Collection<ProxiedPlayer> getPlayers()
    {
        return Collections.unmodifiableCollection( new HashSet<>( players ) );
    }

    @Override
    public boolean canAccess(CommandSender player)
    {
        Preconditions.checkNotNull( player, "player" );
        return !restricted || player.hasPermission( "bungeecord.server." + name );
    }

    @Override
    public boolean equals(Object obj)
    {
        return ( obj instanceof ServerInfo ) && Objects.equal( getAddress(), ( (ServerInfo) obj ).getAddress() );
    }

    @Override
    public int hashCode()
    {
        return address.hashCode();
    }

    @Override
    public void sendData(String channel, byte[] data)
    {
        sendData( channel, data, true );
    }

    // TODO: Don't like this method
    @Override
    public boolean sendData(String channel, byte[] data, boolean queue)
    {
        Preconditions.checkNotNull( channel, "channel" );
        Preconditions.checkNotNull( data, "data" );

        synchronized ( packetQueue )
        {
            Server server = ( players.isEmpty() ) ? null : players.iterator().next().getServer();
            if ( server != null )
            {
                server.sendData( channel, data );
                return true;
            } else if ( queue )
            {
                packetQueue.add( new PluginMessage( channel, data, false ) );
            }
            return false;
        }
    }

    @Override
    public void ping(final Callback<ServerPing> callback)
    {
        ping( callback, ProxyServer.getInstance().getProtocolVersion() );
    }

    public void ping(final Callback<ServerPing> callback, final int protocolVersion)
    {
        Preconditions.checkNotNull( callback, "callback" );

        ChannelFutureListener listener = new ChannelFutureListener()
        {
            @Override
            public void operationComplete(ChannelFuture future) throws Exception
            {
                if ( future.isSuccess() )
                {
                    future.channel().pipeline().get( HandlerBoss.class ).setHandler( new PingHandler( BungeeServerInfo.this, callback, protocolVersion ) );
                } else
                {
                    callback.done( null, future.cause() );
                }
            }
        };
        new Bootstrap()
                .channel( PipelineUtils.getChannel() )
                .group( BungeeCord.getInstance().eventLoops )
                .handler( PipelineUtils.BASE )
                .option( ChannelOption.CONNECT_TIMEOUT_MILLIS, 5000 ) // TODO: Configurable
                .remoteAddress( getAddress() )
                .connect()
                .addListener( listener );
    }
}

@@ -2895,44 +2895,54 @@
     
-synchronized ( packetQueue )%0A       
+Server server;%0A%0A        synchronized (players)
  %7B%0A 
@@ -2956,23 +2956,16 @@
     
-Server 
 server =
  ( p
@@ -2960,18 +2960,16 @@
 server =
- (
  players
@@ -2978,18 +2978,16 @@
 sEmpty()
- )
  ? null 
@@ -3027,24 +3027,31 @@
 rver();%0A    
+    %7D%0A%0A
         if (
@@ -3050,17 +3050,16 @@
     if (
- 
 server !
@@ -3064,36 +3064,20 @@
  != null
- )%0A            %7B%0A   
+) %7B%0A
         
@@ -3076,25 +3076,24 @@
             
- 
 server.sendD
@@ -3088,33 +3088,32 @@
 server.sendData(
- 
 channel, data );
@@ -3109,17 +3109,16 @@
 el, data
- 
 );%0A     
@@ -3120,28 +3120,24 @@
             
-  
-  
 return true;
@@ -3141,28 +3141,24 @@
 ue;%0A        
-  
-  
 %7D else if ( 
@@ -3160,16 +3160,16 @@
 if (
- 
 queue
- 
 )
+ %7B
 %0A   
@@ -3168,32 +3168,59 @@
 e) %7B%0A           
+ synchronized (packetQueue)
  %7B%0A             
@@ -3238,17 +3238,16 @@
 eue.add(
- 
 new Plug
@@ -3256,17 +3256,16 @@
 Message(
- 
 channel,
@@ -3276,19 +3276,17 @@
 a, false
- ) 
+)
 );%0A     
@@ -3290,32 +3290,39 @@
           %7D%0A    
+    %7D%0A%0A
         return f
@@ -3327,26 +3327,16 @@
  false;%0A
-        %7D%0A
     %7D%0A%0A 

Resolve sendData() deadlocks