package net.md_5.bungee.connection;

import com.google.common.io.ByteArrayDataInput;
import com.google.common.io.ByteArrayDataOutput;
import com.google.common.io.ByteStreams;

import java.beans.ConstructorProperties;
import java.io.DataInput;
import java.net.InetAddress;
import java.util.Objects;

import io.netty.buffer.Unpooled;
import net.md_5.bungee.*;
import net.md_5.bungee.api.ProxyServer;
import net.md_5.bungee.api.config.ServerInfo;
import net.md_5.bungee.api.connection.ProxiedPlayer;
import net.md_5.bungee.api.event.PluginMessageEvent;
import net.md_5.bungee.api.event.ServerKickEvent;
import net.md_5.bungee.api.score.Objective;
import net.md_5.bungee.api.score.Position;
import net.md_5.bungee.api.score.Score;
import net.md_5.bungee.api.score.Scoreboard;
import net.md_5.bungee.api.score.Team;
import net.md_5.bungee.netty.ChannelWrapper;
import net.md_5.bungee.netty.PacketHandler;
import net.md_5.bungee.netty.PacketWrapper;
import net.md_5.bungee.protocol.packet.Packet0KeepAlive;
import net.md_5.bungee.protocol.packet.PacketC9PlayerListItem;
import net.md_5.bungee.protocol.packet.PacketCEScoreboardObjective;
import net.md_5.bungee.protocol.packet.PacketCFScoreboardScore;
import net.md_5.bungee.protocol.packet.PacketD0DisplayScoreboard;
import net.md_5.bungee.protocol.packet.PacketD1Team;
import net.md_5.bungee.protocol.packet.PacketFAPluginMessage;
import net.md_5.bungee.protocol.packet.PacketFFKick;

public class DownstreamBridge extends PacketHandler
{

    private final ProxyServer bungee;
    private final UserConnection con;
    private final ServerConnection server;

    @Override
    public void exception(Throwable t) throws Exception
    {
        ServerInfo def = bungee.getServerInfo( con.getPendingConnection().getListener().getFallbackServer() );
        if ( server.getInfo() != def )
        {
            server.setObsolete( true );
            con.connectNow( def );
            con.sendMessage( bungee.getTranslation( "server_went_down" ) );
        } else
        {
            con.disconnect( Util.exception( t ) );
        }
    }

    @Override
    public void disconnected(ChannelWrapper channel) throws Exception
    {
        // We lost connection to the server
        server.getInfo().removePlayer( con );
        if ( bungee.getReconnectHandler() != null )
        {
            bungee.getReconnectHandler().setServer( con );
        }

        if ( !server.isObsolete() )
        {
            con.disconnect( bungee.getTranslation( "lost_connection" ) );
        }
    }

    @Override
    public void handle(final byte[] buf) throws Exception {
        if (!this.server.isObsolete()) {
            EntityMap.rewrite(Unpooled.wrappedBuffer(buf), this.con.getServerEntityId(), this.con.getClientEntityId());
            this.con.sendPacket(buf);
        }
    }

    @Override
    public void handle(Packet0KeepAlive alive) throws Exception
    {
        con.setSentPingId( alive.getRandomId() );
        con.setSentPingTime( System.currentTimeMillis() );
    }

    @Override
    public void handle(PacketC9PlayerListItem playerList) throws Exception
    {

        if ( !con.getTabList().onListUpdate( playerList.getUsername(), playerList.isOnline(), playerList.getPing() ) )
        {
            throw new CancelSendSignal();
        }
    }

    @Override
    public void handle(PacketCEScoreboardObjective objective) throws Exception
    {
        Scoreboard serverScoreboard = con.getServerSentScoreboard();
        switch ( objective.getAction() )
        {
            case 0:
                serverScoreboard.addObjective( new Objective( objective.getName(), objective.getText() ) );
                break;
            case 1:
                serverScoreboard.removeObjective( objective.getName() );
                break;
        }
    }

    @Override
    public void handle(PacketCFScoreboardScore score) throws Exception
    {
        Scoreboard serverScoreboard = con.getServerSentScoreboard();
        switch ( score.getAction() )
        {
            case 0:
                Score s = new Score( score.getItemName(), score.getScoreName(), score.getValue() );
                serverScoreboard.removeScore( score.getItemName() );
                serverScoreboard.addScore( s );
                break;
            case 1:
                serverScoreboard.removeScore( score.getItemName() );
                break;
        }
    }

    @Override
    public void handle(PacketD0DisplayScoreboard displayScoreboard) throws Exception
    {
        Scoreboard serverScoreboard = con.getServerSentScoreboard();
        serverScoreboard.setName( displayScoreboard.getName() );
        serverScoreboard.setPosition( Position.values()[displayScoreboard.getPosition()] );
    }

    @Override
    public void handle(PacketD1Team team) throws Exception
    {
        Scoreboard serverScoreboard = con.getServerSentScoreboard();
        // Remove team and move on
        if ( team.getMode() == 1 )
        {
            serverScoreboard.removeTeam( team.getName() );
            return;
        }

        // Create or get old team
        Team t;
        if ( team.getMode() == 0 )
        {
            t = new Team( team.getName() );
            serverScoreboard.addTeam( t );
        } else
        {
            t = serverScoreboard.getTeam( team.getName() );
        }

        if ( t != null )
        {
            if ( team.getMode() == 0 || team.getMode() == 2 )
            {
                t.setDisplayName( team.getDisplayName() );
                t.setPrefix( team.getPrefix() );
                t.setSuffix( team.getSuffix() );
                t.setFriendlyFire( team.isFriendlyFire() );
            }
            if ( team.getPlayers() != null )
            {
                for ( String s : team.getPlayers() )
                {
                    if ( team.getMode() == 0 || team.getMode() == 3 )
                    {
                        t.addPlayer( s );
                    } else
                    {
                        t.removePlayer( s );
                    }
                }
            }
        }
    }

    @Override
    public void handle(final PacketFAPluginMessage pluginMessage) throws Exception {
        final ByteArrayDataInput in = ByteStreams.newDataInput(pluginMessage.getData());
        final PluginMessageEvent event = new PluginMessageEvent(this.con.getServer(), this.con, pluginMessage.getTag(), pluginMessage.getData().clone());
        if (this.bungee.getPluginManager().callEvent(event).isCancelled()) {
            throw new CancelSendSignal();
        }
        if (pluginMessage.getTag().equals("MC|TPack") && this.con.getPendingConnection().getListener().getTexturePack() != null) {
            throw new CancelSendSignal();
        }
        if (pluginMessage.getTag().equals("BungeeCord")) {
            ByteArrayDataOutput out = ByteStreams.newDataOutput();
            final String subChannel = in.readUTF();
            if (subChannel.equals("Forward")) {
                final String target = in.readUTF();
                final String channel = in.readUTF();
                final short len = in.readShort();
                final byte[] data = new byte[len];
                in.readFully(data);
                out.writeUTF(channel);
                out.writeShort(data.length);
                out.write(data);
                final byte[] payload = out.toByteArray();
                out = null;
                if (target.equals("ALL")) {
                    for (final ServerInfo server : this.bungee.getServers().values()) {
                        if (server != this.con.getServer().getInfo()) {
                            server.sendData("BungeeCord", payload);
                        }
                    }
                } else {
                    final ServerInfo server2 = this.bungee.getServerInfo(target);
                    if (server2 != null) {
                        server2.sendData("BungeeCord", payload);
                    }
                }
            }
            if (subChannel.equals("Connect")) {
                final ServerInfo server3 = this.bungee.getServerInfo(in.readUTF());
                if (server3 != null) {
                    this.con.connect(server3);
                }
            }
            if (subChannel.equals("IP")) {
                out.writeUTF("IP");
                Object ob = this.con.getAttachment().get("remoteAddr");
                if(ob != null && (ob instanceof InetAddress)) {
                    out.writeUTF(((InetAddress)ob).getHostAddress());
                    out.writeInt(this.con.getAddress().getPort());
                }else {
                    out.writeUTF(this.con.getAddress().getHostString());
                    out.writeInt(this.con.getAddress().getPort());
                }
            }
            if (subChannel.equals("PlayerCount")) {
                final String target = in.readUTF();
                out.writeUTF("PlayerCount");
                if (target.equals("ALL")) {
                    out.writeUTF("ALL");
                    out.writeInt(this.bungee.getOnlineCount());
                } else {
                    final ServerInfo server4 = this.bungee.getServerInfo(target);
                    if (server4 != null) {
                        out.writeUTF(server4.getName());
                        out.writeInt(server4.getPlayers().size());
                    }
                }
            }
            if (subChannel.equals("PlayerList")) {
                final String target = in.readUTF();
                out.writeUTF("PlayerList");
                if (target.equals("ALL")) {
                    out.writeUTF("ALL");
                    out.writeUTF(Util.csv(this.bungee.getPlayers()));
                } else {
                    final ServerInfo server4 = this.bungee.getServerInfo(target);
                    if (server4 != null) {
                        out.writeUTF(server4.getName());
                        out.writeUTF(Util.csv(server4.getPlayers()));
                    }
                }
            }
            if (subChannel.equals("GetServers")) {
                out.writeUTF("GetServers");
                out.writeUTF(Util.csv(this.bungee.getServers().keySet()));
            }
            if (subChannel.equals("Message")) {
                final ProxiedPlayer target2 = this.bungee.getPlayer(in.readUTF());
                if (target2 != null) {
                    target2.sendMessage(in.readUTF());
                }
            }
            if (subChannel.equals("GetServer")) {
                out.writeUTF("GetServer");
                out.writeUTF(this.server.getInfo().getName());
            }
            if (subChannel.equals("EAG|GetDomain")) {
                out.writeUTF("EAG|GetDomain");
                Object ob = this.con.getAttachment().get("origin");
                if(ob != null && (ob instanceof String)) {
                    out.writeBoolean(true);
                    out.writeUTF((String)ob);
                }else {
                    out.writeBoolean(false);
                    out.writeUTF("");
                }
            }
            if (subChannel.equals("EAG|ConsoleCommand")) {
                if(BungeeCord.getInstance().config.shouldAcceptBukkitConsoleCommandPacket()) {
                    String cmd = in.readUTF();
                    bungee.getLogger().info("Connection [" + this.con.getName() + "] <-> [" + this.server.getInfo().getName() + "] executed bungee console command: " + cmd);
                    bungee.getPluginManager().dispatchCommand(bungee.getConsole(), cmd);
                }else {
                    bungee.getLogger().info("Connection [" + this.con.getName() + "] <-> [" + this.server.getInfo().getName() + "] tried executing a bungee console command but \"accept_bukkit_console_command_packets\" is set to false in config.yml");
                }
            }
            if (out != null) {
                final byte[] b = out.toByteArray();
                if (b.length != 0) {
                    this.con.getServer().sendData("BungeeCord", b);
                }
            }
        }
    }

    @Override
    public void handle(PacketFFKick kick) throws Exception
    {
        ServerInfo def = bungee.getServerInfo( con.getPendingConnection().getListener().getFallbackServer() );
        if ( Objects.equals( server.getInfo(), def ) )
        {
            def = null;
        }
        ServerKickEvent event = bungee.getPluginManager().callEvent( new ServerKickEvent( con, kick.getMessage(), def, ServerKickEvent.State.CONNECTED ) );
        if ( event.isCancelled() && event.getCancelServer() != null )
        {
            con.connectNow( event.getCancelServer() );
        } else
        {
            con.disconnect( bungee.getTranslation( "server_kick" ) + event.getKickReason() );
        }
        server.setObsolete( true );
        throw new CancelSendSignal();
    }

    @Override
    public String toString()
    {
        return "[" + con.getName() + "] <-> DownstreamBridge <-> [" + server.getInfo().getName() + "]";
    }

    @ConstructorProperties({ "bungee", "con", "server" })
    public DownstreamBridge(final ProxyServer bungee, final UserConnection con, final ServerConnection server) {
        this.bungee = bungee;
        this.con = con;
        this.server = server;
    }
}
