/*
 *   This file is a part of the SteamWar software.
 *
 *   Copyright (C) 2025 SteamWar.de-Serverteam
 *
 *   This program is free software: you can redistribute it and/or modify
 *   it under the terms of the GNU Affero General Public License as published by
 *   the Free Software Foundation, either version 3 of the License, or
 *   (at your option) any later version.
 *
 *   This program is distributed in the hope that it will be useful,
 *   but WITHOUT ANY WARRANTY; without even the implied warranty of
 *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 *   GNU Affero General Public License for more details.
 *
 *   You should have received a copy of the GNU Affero General Public License
 *   along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */

package de.steamwar.velocitycore.tablist;

import com.velocitypowered.api.network.ProtocolVersion;
import com.velocitypowered.api.proxy.Player;
import com.velocitypowered.api.util.GameProfile;
import com.velocitypowered.proxy.connection.backend.VelocityServerConnection;
import com.velocitypowered.proxy.connection.client.ConnectedPlayer;
import com.velocitypowered.proxy.network.Connections;
import com.velocitypowered.proxy.protocol.MinecraftPacket;
import com.velocitypowered.proxy.protocol.packet.LegacyPlayerListItemPacket;
import com.velocitypowered.proxy.protocol.packet.RemovePlayerInfoPacket;
import com.velocitypowered.proxy.protocol.packet.UpdateTeamsPacket;
import com.velocitypowered.proxy.protocol.packet.UpsertPlayerInfoPacket;
import com.velocitypowered.proxy.protocol.packet.chat.ComponentHolder;
import de.steamwar.messages.Chatter;
import de.steamwar.persistent.Storage;
import de.steamwar.velocitycore.VelocityCore;
import io.netty.channel.ChannelHandler;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.channel.ChannelPipeline;
import io.netty.util.ReferenceCountUtil;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.NamedTextColor;

import java.util.*;
import java.util.logging.Level;
import java.util.stream.IntStream;

@ChannelHandler.Sharable
public class Tablist extends ChannelInboundHandlerAdapter {

    private static final UUID[] swUuids = IntStream.range(0, 80).mapToObj(i -> UUID.randomUUID()).toArray(UUID[]::new);
    private static final String[] swNames = IntStream.range(0, 80).mapToObj(i -> " »SW« " + String.format("%02d", i)).toArray(String[]::new);
    public static final UpdateTeamsPacket createTeamPacket = new UpdateTeamsPacket21("zzzzzsw-tab", UpdateTeamsPacket.Mode.CREATE, Component.empty(), Component.empty(), Component.empty(), UpdateTeamsPacket.NameTagVisibility.NEVER, UpdateTeamsPacket.CollisionRule.ALWAYS, 21, (byte) 0x00, Arrays.stream(Tablist.swNames).toList());

    private final Map<UUID, UpsertPlayerInfoPacket.Entry> directTabItems;
    private final List<UpsertPlayerInfoPacket.Entry> current = new ArrayList<>();

    private final Player player;
    private final Chatter viewer;
    private VelocityServerConnection connection;

    public Tablist(Player player) {
        this.player = player;
        this.viewer = Chatter.of(player);
        this.directTabItems = Storage.directTabItems.computeIfAbsent(player, p -> new HashMap<>());
    }

    public void update(TablistPart global, int seconds) {
        if (connection == null) return;

        player.sendPlayerListHeaderAndFooter(header(viewer, seconds), viewer.parse(false, "TABLIST_FOOTER", connection.getServerInfo().getName(), ping(), VelocityCore.getProxy().getPlayerCount()));

        List<TablistPart.Item> tablist = new ArrayList<>();
        List<TablistPart.Item> direct = new ArrayList<>();
        global.print(viewer, player, tablist, direct);
        tablist.addAll(0, direct);

        // Main list handling
        int i = 0;
        List<UpsertPlayerInfoPacket.Entry> add = new ArrayList<>();
        List<UpsertPlayerInfoPacket.Entry> update = new ArrayList<>();
        List<UpsertPlayerInfoPacket.Entry> remove = new ArrayList<>();
        for (; i < tablist.size() && i < 80; i++) {
            TablistPart.Item item = tablist.get(i);
            UpsertPlayerInfoPacket.Entry tabItem;
            if (current.size() > i) {
                tabItem = current.get(i);
            } else {
                tabItem = new UpsertPlayerInfoPacket.Entry(swUuids[i]);
                tabItem.setProfile(new GameProfile(swUuids[i], swNames[i], Collections.emptyList()));
                tabItem.setGameMode(1);
                tabItem.setListed(true);
                tabItem.setLatency(1000);
                current.add(tabItem);
            }

            if (!tabItem.getProfile().getProperties().equals(item.getProperties())) {
                tabItem.setProfile(new GameProfile(tabItem.getProfile().getId(), tabItem.getProfile().getName(), item.getProperties()));
                tabItem.setDisplayName(new ComponentHolder(player.getProtocolVersion(), item.getDisplayName()));
                add.add(tabItem);
                if (current.size() > i) {
                    remove.add(tabItem);
                }
            } else if (!item.getDisplayName().equals(getDisplayName(tabItem))) {
                tabItem.setDisplayName(new ComponentHolder(player.getProtocolVersion(), item.getDisplayName()));
                update.add(tabItem);
            }
        }

        // Excess removal
        while (i < current.size()) {
            remove.add(current.remove(i));
        }

        sendTabPacket(remove, null);
        sendTabPacket(update, UpsertPlayerInfoPacket.Action.UPDATE_DISPLAY_NAME);
        sendTabPacket(add, UpsertPlayerInfoPacket.Action.ADD_PLAYER);
    }

    public void onServerSwitch() {
        injection();

        synchronized (directTabItems) {
            directTabItems.clear();
        }
    }

    public void onServerPostSwitch() {
        if (player.getProtocolVersion().noLessThan(ProtocolVersion.MINECRAFT_1_20)) {
            if (player.getProtocolVersion().greaterThan(ProtocolVersion.MINECRAFT_1_20)) {
                sendTabPacket(current, null);
                current.clear();
            }

            sendPacket(player, createTeamPacket);
        }
    }

    private void injection() {
        connection = (VelocityServerConnection) player.getCurrentServer().orElse(null);
        if (connection == null) {
            connection = ((ConnectedPlayer) player).getConnectionInFlight();

            if (connection == null) {
                VelocityCore.getLogger().log(Level.WARNING, "Could not inject Tablist: %s".formatted(player));
                return;
            }
        }

        ChannelPipeline pipeline = connection.getConnection().getChannel().pipeline();
        if (pipeline.get("steamwar-tablist") != null) //Catch unclean exit
            pipeline.remove("steamwar-tablist");

        pipeline.addBefore(Connections.HANDLER, "steamwar-tablist", this);
    }

    public void disable() {
        sendTabPacket(new ArrayList<>(directTabItems.values()), null);
        sendTabPacket(current, null);
        current.clear();

        if (connection != null) {
            connection.getConnection().getChannel().pipeline().remove(this);
            connection = null;
        }
    }

    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) {
        if (msg instanceof UpsertPlayerInfoPacket packet) {
            packet.getActions().remove(UpsertPlayerInfoPacket.Action.INITIALIZE_CHAT);
            packet.getActions().remove(UpsertPlayerInfoPacket.Action.UPDATE_LATENCY);
            packet.getActions().remove(UpsertPlayerInfoPacket.Action.UPDATE_LISTED);
            if (packet.getActions().isEmpty()) {
                ReferenceCountUtil.release(msg);
                return;
            }

            if (packet.containsAction(UpsertPlayerInfoPacket.Action.ADD_PLAYER)) {
                for (UpsertPlayerInfoPacket.Entry entry : packet.getEntries()) {
                    entry.setLatency(1);
                    entry.setChatSession(null);

                    if (!Storage.teamServers.containsValue(connection.getServerInfo())) {
                        entry.setDisplayName(new ComponentHolder(player.getProtocolVersion(), Component.empty()));
                        entry.setListed(false);
                    } else if (entry.getDisplayName() == null) {
                        entry.setDisplayName(new ComponentHolder(player.getProtocolVersion(), Component.text(entry.getProfile().getName()).color(NamedTextColor.GRAY)));
                    }

                    synchronized (directTabItems) {
                        directTabItems.put(entry.getProfileId(), entry);
                    }
                }

                packet.addAction(UpsertPlayerInfoPacket.Action.UPDATE_LISTED);
                packet.addAction(UpsertPlayerInfoPacket.Action.UPDATE_LATENCY);
                packet.addAction(UpsertPlayerInfoPacket.Action.UPDATE_DISPLAY_NAME);
                packet.addAction(UpsertPlayerInfoPacket.Action.UPDATE_GAME_MODE);
            }

            if (packet.containsAction(UpsertPlayerInfoPacket.Action.UPDATE_GAME_MODE)) {
                for (UpsertPlayerInfoPacket.Entry entry : packet.getEntries()) {
                    if (!entry.getProfileId().equals(player.getUniqueId()) && entry.getGameMode() == 3) {
                        entry.setGameMode(1);
                    }
                }
            }
        } else if (msg instanceof RemovePlayerInfoPacket packet) {
            for (UUID uuid : packet.getProfilesToRemove()) {
                synchronized (directTabItems) {
                    directTabItems.remove(uuid);
                }
            }
        } else if (msg instanceof LegacyPlayerListItemPacket packet) {
            if (packet.getAction() == LegacyPlayerListItemPacket.ADD_PLAYER) {
                for (LegacyPlayerListItemPacket.Item entry : packet.getItems()) {
                    entry.setLatency(1).setPlayerKey(null);

                    if (!Storage.teamServers.containsValue(connection.getServerInfo())) {
                        entry.setDisplayName(Component.empty());
                    } else if (entry.getDisplayName() == null) {
                        entry.setDisplayName(Component.text(entry.getName()).color(NamedTextColor.GRAY));
                    }

                    UpsertPlayerInfoPacket.Entry newEntry = new UpsertPlayerInfoPacket.Entry(entry.getUuid());
                    newEntry.setProfile(new GameProfile(entry.getUuid(), entry.getName(), entry.getProperties()));
                    newEntry.setDisplayName(new ComponentHolder(player.getProtocolVersion(), entry.getDisplayName()));
                    newEntry.setListed(true);
                    synchronized (directTabItems) {
                        directTabItems.put(entry.getUuid(), newEntry);
                    }
                }
            } else if (packet.getAction() == LegacyPlayerListItemPacket.UPDATE_GAMEMODE) {
                for (LegacyPlayerListItemPacket.Item entry : packet.getItems()) {
                    if (!player.getUniqueId().equals(entry.getUuid()) && entry.getGameMode() == 3) {
                        entry.setGameMode(1);
                    }
                }
            } else if (packet.getAction() == LegacyPlayerListItemPacket.REMOVE_PLAYER) {
                for (LegacyPlayerListItemPacket.Item entry : packet.getItems()) {
                    synchronized (directTabItems) {
                        directTabItems.remove(entry.getUuid());
                    }
                }
            } else {
                ReferenceCountUtil.release(msg);
                return;
            }
        }

        ctx.fireChannelRead(msg);
    }

    private void sendTabPacket(List<UpsertPlayerInfoPacket.Entry> items, UpsertPlayerInfoPacket.Action action) { //Breaks in 1.19.3 if action != UPDATE_DISPLAY_NAME, ADD_PLAYER or REMOVE_PLAYER
        if (items.isEmpty()) return;

        if (player.getProtocolVersion().lessThan(ProtocolVersion.MINECRAFT_1_19_3)) {
            int legacyAction = LegacyPlayerListItemPacket.REMOVE_PLAYER;
            if (action == UpsertPlayerInfoPacket.Action.UPDATE_DISPLAY_NAME) {
                legacyAction = LegacyPlayerListItemPacket.UPDATE_DISPLAY_NAME;
            } else if (action == UpsertPlayerInfoPacket.Action.ADD_PLAYER) {
                legacyAction = LegacyPlayerListItemPacket.ADD_PLAYER;
            }

            sendPacket(player, new LegacyPlayerListItemPacket(legacyAction, items.stream().map(item -> new LegacyPlayerListItemPacket
                    .Item(item.getProfileId())
                    .setName(item.getProfile().getName())
                    .setProperties(item.getProfile().getProperties())
                    .setDisplayName(item.getDisplayName().getComponent())
                    .setLatency(item.getLatency())
                    .setGameMode(item.getGameMode())).toList()));
            return;
        }

        if (action == null) { //REMOVE
            sendPacket(player, new RemovePlayerInfoPacket(items.stream().map(item -> item.getProfile().getId()).toList()));
            return;
        }

        sendPacket(player, new UpsertPlayerInfoPacket(
                action == UpsertPlayerInfoPacket.Action.UPDATE_DISPLAY_NAME ? EnumSet.of(UpsertPlayerInfoPacket.Action.UPDATE_DISPLAY_NAME, UpsertPlayerInfoPacket.Action.UPDATE_LISTED) : EnumSet.of(UpsertPlayerInfoPacket.Action.ADD_PLAYER, UpsertPlayerInfoPacket.Action.UPDATE_DISPLAY_NAME, UpsertPlayerInfoPacket.Action.UPDATE_LATENCY, UpsertPlayerInfoPacket.Action.UPDATE_LISTED, UpsertPlayerInfoPacket.Action.UPDATE_GAME_MODE),
                items
        ));
    }

    private Component header(Chatter p, int seconds) {
        int phase = (seconds % 10) / 5;
        if (phase == 0) {
            return p.parse(false, "TABLIST_PHASE_DISCORD");
        } else {
            return p.parse(false, "TABLIST_PHASE_WEBSITE");
        }
    }

    private String ping() {
        long ping = player.getPing();
        if (ping == -1) {
            return "§7?";
        } else if (ping < 50) {
            return "§a" + ping;
        } else if (ping < 150) {
            return "§e" + ping;
        } else {
            return "§c" + ping;
        }
    }

    private Component getDisplayName(UpsertPlayerInfoPacket.Entry entry) {
        ComponentHolder displayName = entry.getDisplayName();
        if (displayName == null) return null;

        return displayName.getComponent();
    }

    public static void sendPacket(Player player, MinecraftPacket packet) {
        ((ConnectedPlayer) player).getConnection().write(packet);
    }
}
