package net.md_5.bungee;

import com.google.common.base.Preconditions;
import io.netty.bootstrap.Bootstrap;
import io.netty.channel.*;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.util.concurrent.GenericFutureListener;
import io.netty.util.internal.PlatformDependent;

import java.beans.ConstructorProperties;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.nio.charset.StandardCharsets;
import java.util.*;
import java.util.logging.Level;
import net.md_5.bungee.api.ChatColor;
import net.md_5.bungee.api.ProxyServer;
import net.md_5.bungee.api.config.ServerInfo;
import net.md_5.bungee.api.connection.Connection;
import net.md_5.bungee.api.connection.ProxiedPlayer;
import net.md_5.bungee.api.event.PermissionCheckEvent;
import net.md_5.bungee.api.event.ServerConnectEvent;
import net.md_5.bungee.api.score.Scoreboard;
import net.md_5.bungee.api.tab.TabListHandler;
import net.md_5.bungee.connection.InitialHandler;
import net.md_5.bungee.eaglercraft.RedirectServerInfo;
import net.md_5.bungee.netty.ChannelWrapper;
import net.md_5.bungee.netty.HandlerBoss;
import net.md_5.bungee.netty.PacketWrapper;
import net.md_5.bungee.netty.PipelineUtils;
import net.md_5.bungee.protocol.packet.DefinedPacket;
import net.md_5.bungee.protocol.packet.Packet3Chat;
import net.md_5.bungee.protocol.packet.PacketCCSettings;
import net.md_5.bungee.protocol.packet.PacketFAPluginMessage;
import net.md_5.bungee.protocol.packet.PacketFFKick;
import net.md_5.bungee.util.CaseInsensitiveSet;

public final class UserConnection implements ProxiedPlayer
{

    /*========================================================================*/
    private final ProxyServer bungee;
    private final ChannelWrapper ch;
    private final String name;
    private final InitialHandler pendingConnection;
    /*========================================================================*/
    private ServerConnection server;
    private Object switchMutex = new Object();
    private Collection<ServerInfo> pendingConnects = new HashSet<>();
    /*========================================================================*/
    private TabListHandler tabList;
    private int sentPingId;

    private long sentPingTime;
    private int ping = 100;
    private ServerInfo reconnectServer;
    /*========================================================================*/
    private Collection<String> groups = new CaseInsensitiveSet();
    private Collection<String> permissions = new CaseInsensitiveSet();
    /*========================================================================*/


    private int serverEntityId;
    private PacketCCSettings settings;

    private Scoreboard serverSentScoreboard = new Scoreboard();
    /*========================================================================*/

    private String displayName;
    /*========================================================================*/

    private int clientEntityId;

    private final Map<String, Object> attachment = new WeakHashMap();

    public void setServer(ServerConnection server) {
        this.server = server;
    }

    public void setSettings(PacketCCSettings settings){
        this.settings = settings;
    }

    public void setServerEntityId(int serverEntityId) {
        this.serverEntityId = serverEntityId;
    }

    public void setSentPingId(int sentPingId) {
        this.sentPingId = sentPingId;
    }

    public void setSentPingTime(long sentPingTime) {
        this.sentPingTime = sentPingTime;
    }

    public void setPing(int ping) {
        this.ping = ping;
    }

    @Override
    public void setReconnectServer(ServerInfo reconnectServer) {
        this.reconnectServer = reconnectServer;
    }

    public void setClientEntityId(int clientEntityId) {
        this.clientEntityId = clientEntityId;
    }

    @Override
    public String getName() {
        return name;
    }

    @Override
    public InitialHandler getPendingConnection() {
        return pendingConnection;
    }

    @Override
    public TabListHandler getTabList() {
        return tabList;
    }

    public Collection<ServerInfo> getPendingConnects() {
        return pendingConnects;
    }

    public Object getSwitchMutex() {
        return switchMutex;
    }

    @Override
    public ServerConnection getServer() {
        return server;
    }

    public int getSentPingId() {
        return sentPingId;
    }

    public long getSentPingTime() {
        return sentPingTime;
    }

    @Override
    public int getPing() {
        return ping;
    }

    @Override
    public ServerInfo getReconnectServer() {
        return reconnectServer;
    }

    public Collection<String> getPermissions() {
        return permissions;
    }

    public int getClientEntityId() {
        return clientEntityId;
    }

    public int getServerEntityId() {
        return serverEntityId;
    }

    public PacketCCSettings getSettings() {
        return settings;
    }

    public Scoreboard getServerSentScoreboard() {
        return serverSentScoreboard;
    }

    @Override
    public String getDisplayName() {
        return displayName;
    }

    private final Connection.Unsafe unsafe;

    @ConstructorProperties({ "bungee", "ch", "name", "pendingConnection" })
    public UserConnection(final ProxyServer bungee, final ChannelWrapper ch, final String name, final InitialHandler pendingConnection) {
        this.switchMutex = new Object();
        this.pendingConnects = new HashSet<ServerInfo>();
        this.ping = 100;
        this.groups = (Collection<String>) new CaseInsensitiveSet();
        this.permissions = (Collection<String>) new CaseInsensitiveSet();
        this.serverSentScoreboard = new Scoreboard();
        this.unsafe = new Connection.Unsafe() {
            @Override
            public void sendPacket(final DefinedPacket packet) {
                UserConnection.this.ch.write(packet);
            }
        };
        if (bungee == null) {
            throw new NullPointerException("bungee");
        }
        if (ch == null) {
            throw new NullPointerException("ch");
        }
        if (name == null) {
            throw new NullPointerException("name");
        }
        this.bungee = bungee;
        this.ch = ch;
        this.name = name;
        this.pendingConnection = pendingConnection;
    }

    public void init()
    {
        this.displayName = name;
        try
        {
            this.tabList = getPendingConnection().getListener().getTabList().getDeclaredConstructor().newInstance();
        } catch ( ReflectiveOperationException ex )
        {
            throw new RuntimeException( ex );
        }
        this.tabList.init( this );

        Collection<String> g = bungee.getConfigurationAdapter().getGroups( name );
        for ( String s : g )
        {
            addGroups( s );
        }
    }

    @Override
    public void setTabList(TabListHandler tabList)
    {
        tabList.init( this );
        this.tabList = tabList;
    }

    public void sendPacket(final byte[] b) {
        this.ch.write(b);
    }


    @Deprecated
    public boolean isActive()
    {
        return !ch.isClosed();
    }

    @Override
    public void setDisplayName(String name)
    {
        Preconditions.checkNotNull( name, "displayName" );
        Preconditions.checkArgument( name.length() <= 16, "Display name cannot be longer than 16 characters" );
        getTabList().onDisconnect();
        displayName = name;
        getTabList().onConnect();
    }

    @Override
    public void connect(ServerInfo target)
    {
        connect( target, false );
    }

    void sendDimensionSwitch()
    {
        unsafe().sendPacket( PacketConstants.DIM1_SWITCH );
        unsafe().sendPacket( PacketConstants.DIM2_SWITCH );
    }

    public void connectNow(ServerInfo target)
    {
        sendDimensionSwitch();
        connect( target );
    }

    public void connect(final ServerInfo info, final boolean retry) {
		if(info instanceof RedirectServerInfo) {
			sendData("EAG|Reconnect", ((RedirectServerInfo)info).getRedirect().getBytes(StandardCharsets.UTF_8));
			return;
		}
		final ServerConnectEvent event = new ServerConnectEvent(this, info);
		if (this.bungee.getPluginManager().callEvent(event).isCancelled()) {
			return;
		}
		final BungeeServerInfo target = (BungeeServerInfo) event.getTarget();
		if (this.getServer() != null && this.getServer().getInfo() != null && Objects.equals(this.getServer().getInfo(), target)) {
			//this.sendMessage(ChatColor.RED + "Cannot connect to server you are already on!");
			return;
		}
		if (this.pendingConnects.contains(target)) {
			this.sendMessage(ChatColor.RED + "Already connecting to this server!");
			return;
		}
		this.pendingConnects.add(target);
		final ChannelInitializer initializer = new ChannelInitializer() {
			protected void initChannel(final Channel ch) throws Exception {
				PipelineUtils.BASE.initChannel(ch);
				((HandlerBoss) ch.pipeline().get((Class) HandlerBoss.class)).setHandler(new ServerConnector(UserConnection.this.bungee, UserConnection.this, target));
			}
		};
		final ChannelFutureListener listener = (ChannelFutureListener) new ChannelFutureListener() {
			public void operationComplete(final ChannelFuture future) throws Exception {
				if (!future.isSuccess()) {
					future.channel().close();
					UserConnection.this.pendingConnects.remove(target);
					final ServerInfo def = ProxyServer.getInstance().getServers().get(UserConnection.this.getPendingConnection().getListener().getFallbackServer());
					if ((retry & target != def) && ((UserConnection.this.getServer() == null || UserConnection.this.getServer().getInfo() == null) || def != UserConnection.this.getServer().getInfo())) {
						UserConnection.this.sendMessage(UserConnection.this.bungee.getTranslation("fallback_lobby"));
						UserConnection.this.connect(def, false);
					} else if (UserConnection.this.getServer() == null || UserConnection.this.getServer().getInfo() == null) {
						UserConnection.this.disconnect(UserConnection.this.bungee.getTranslation("fallback_kick") + future.cause().getClass().getName());
					} else {
						UserConnection.this.sendMessage(UserConnection.this.bungee.getTranslation("fallback_kick") + future.cause().getClass().getName());
					}
				}
			}
		};
		final Bootstrap b = ((Bootstrap) ((Bootstrap) ((Bootstrap) ((Bootstrap) new Bootstrap().channel((Class) NioSocketChannel.class)).group((EventLoopGroup) BungeeCord.getInstance().eventLoops)).handler((ChannelHandler) initializer))
				.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, Integer.valueOf(5000))).remoteAddress((SocketAddress) target.getAddress());
		if (!PlatformDependent.isWindows()) {
			b.localAddress(this.getPendingConnection().getListener().getHost().getHostString(), 0);
		}
		b.connect().addListener((GenericFutureListener) listener);
	}

    @Override
    public synchronized void disconnect(String reason)
    {
        if ( ch.getHandle().isActive() )
        {
            bungee.getLogger().log( Level.INFO, "[" + getName() + "] disconnected with: " + reason );
            unsafe().sendPacket( new PacketFFKick( reason ) );
            ch.close();
            if ( server != null )
            {
                server.disconnect( "Quitting" );
            }
        }
    }

    @Override
    public void chat(String message)
    {
        Preconditions.checkState( server != null, "Not connected to server" );
        server.getCh().write( new Packet3Chat( message ) );
    }

    @Override
    public void sendMessage(String message)
    {
        this.unsafe().sendPacket(new Packet3Chat(message));
    }

    @Override
    public void sendMessages(String... messages)
    {
        for ( String message : messages )
        {
            sendMessage( message );
        }
    }

    @Override
    public void sendData(String channel, byte[] data)
    {
        unsafe().sendPacket( new PacketFAPluginMessage( channel, data ) );
    }

    @Override
    public InetSocketAddress getAddress()
    {
        return (InetSocketAddress) ch.getHandle().remoteAddress();
    }

    @Override
    public Collection<String> getGroups()
    {
        return Collections.unmodifiableCollection( groups );
    }

    @Override
    public void addGroups(String... groups)
    {
        for ( String group : groups )
        {
            this.groups.add( group );
            for ( String permission : bungee.getConfigurationAdapter().getPermissions( group ) )
            {
                setPermission( permission, true );
            }
        }
    }

    @Override
    public void removeGroups(String... groups)
    {
        for ( String group : groups )
        {
            this.groups.remove( group );
            for ( String permission : bungee.getConfigurationAdapter().getPermissions( group ) )
            {
                setPermission( permission, false );
            }
        }
    }

    @Override
    public boolean hasPermission(String permission)
    {
        return bungee.getPluginManager().callEvent( new PermissionCheckEvent( this, permission, permissions.contains( permission ) ) ).hasPermission();
    }

    @Override
    public void setPermission(String permission, boolean value)
    {
        if ( value )
        {
            permissions.add( permission );
        } else
        {
            permissions.remove( permission );
        }
    }

    @Override
    public Map<String, Object> getAttachment() { // fix this (maybe)
        return attachment;
    }

    @Override
    public String toString()
    {
        return name;
    }

    @Override
    public Unsafe unsafe()
    {
        return unsafe;
    }
}
