/*
    This file is a part of the SteamWar software.

    Copyright (C) 2020  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.fightsystem.record;

import com.sk89q.worldedit.extent.clipboard.Clipboard;
import de.steamwar.core.Core;
import de.steamwar.core.TrickyTrialsWrapper;
import de.steamwar.core.WorldEditWrapper;
import de.steamwar.entity.REntity;
import de.steamwar.entity.REntityServer;
import de.steamwar.entity.RPlayer;
import de.steamwar.fightsystem.ArenaMode;
import de.steamwar.fightsystem.Config;
import de.steamwar.fightsystem.FightSystem;
import de.steamwar.fightsystem.countdown.Countdown;
import de.steamwar.fightsystem.fight.Fight;
import de.steamwar.fightsystem.fight.FightTeam;
import de.steamwar.fightsystem.fight.FightWorld;
import de.steamwar.fightsystem.fight.FreezeWorld;
import de.steamwar.fightsystem.listener.FightScoreboard;
import de.steamwar.fightsystem.states.FightState;
import de.steamwar.fightsystem.utils.*;
import de.steamwar.sql.SchematicData;
import de.steamwar.sql.SchematicNode;
import de.steamwar.sql.SteamwarUser;
import de.steamwar.sql.Team;
import de.steamwar.techhider.BlockIds;
import net.md_5.bungee.api.ChatMessageType;
import net.md_5.bungee.api.chat.BaseComponent;
import net.md_5.bungee.api.chat.TextComponent;
import org.bukkit.Bukkit;
import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.Sound;
import org.bukkit.enchantments.Enchantment;
import org.bukkit.entity.EntityType;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.HandlerList;
import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerJoinEvent;
import org.bukkit.inventory.ItemStack;
import org.bukkit.scheduler.BukkitTask;
import org.bukkit.scoreboard.NameTagVisibility;

import java.io.EOFException;
import java.io.FilterInputStream;
import java.io.IOException;
import java.util.*;
import java.util.logging.Level;
import java.util.stream.Collectors;

public class PacketProcessor implements Listener {

    private static final org.bukkit.scoreboard.Team team = FightScoreboard.getBukkitTeam("Replay");
    static {
        BountifulWrapper.impl.setNametagVisibility(team);
        team.setNameTagVisibility(NameTagVisibility.NEVER);
    }

    private static PacketProcessor currentProcessor = null;

    public static PacketProcessor currentReplay() {
        return currentProcessor;
    }

    public static boolean isReplaying(){
        return currentProcessor != null;
    }

    private final PacketParser[] packetDecoder = new PacketParser[256];

    private final PacketSource source;
    private final BukkitTask task;
    private final LinkedList<Runnable> syncList = new LinkedList<>();
    private final Set<Integer> hiddenBlockIds = Config.HiddenBlocks.stream().flatMap(m -> BlockIds.impl.materialToAllIds(m).stream()).collect(Collectors.toSet());
    private final int obfuscateWith = BlockIds.impl.materialToId(Config.ObfuscateWith);
    private final FreezeWorld freezer = new FreezeWorld();
    private final REntityServer entityServer = new REntityServer();
    private final Map<Integer, REntity> entities = new HashMap<>();

    private boolean rotateZ = false;
    private int arenaMinX = Config.ArenaRegion.getMinX();
    private int arenaMinY = Config.BluePasteRegion.getMinY();
    private int arenaMinZ = Config.ArenaRegion.getMinZ();

    private boolean skipToSubtitle = false;
    
    private boolean tickFinished = false;
    private final List<Integer> lastPackets = new LinkedList<>();

    public PacketProcessor(PacketSource source) {
        this.source = source;
        currentProcessor = this;

        packetDecoder[0x00] = this::playerJoins;
        packetDecoder[0x01] = this::entityMoves;
        packetDecoder[0x02] = this::entityDespawns;
        packetDecoder[0x03] = this::entitySneak;
        packetDecoder[0x04] = this::entityAnimation;
        packetDecoder[0x05] = this::tntSpawn;
        packetDecoder[0x06] = this::entityVelocity;
        packetDecoder[0x07] = this::playerItem;
        packetDecoder[0x08] = this::arrowSpawn;
        packetDecoder[0x09] = this::fireballSpawn;
        packetDecoder[0x0a] = this::bow;
        packetDecoder[0x0b] = this::damage;
        packetDecoder[0x0c] = this::fireTick;
        packetDecoder[0x20] = this::oldArenaInfo;
        packetDecoder[0x21] = this::arenaInfo;
        packetDecoder[0x30] = this::byteWorldHeightBlock;
        packetDecoder[0x31] = this::particle;
        packetDecoder[0x32] = this::sound;
        packetDecoder[0x33] = this::shortBlock;
        packetDecoder[0x34] = this::soundAtPlayer;
        packetDecoder[0x35] = this::shortRelativeBlock;
        packetDecoder[0x36] = this::block;
        packetDecoder[0xa0] = () -> send(ChatMessageType.CHAT);
        packetDecoder[0xa1] = () -> send(ChatMessageType.ACTION_BAR);
        packetDecoder[0xa2] = () -> send(ChatMessageType.SYSTEM);
        packetDecoder[0xa3] = this::countdown;
        packetDecoder[0xa4] = this::chat;
        packetDecoder[0xa5] = this::system;
        packetDecoder[0xb0] = () -> pasteSchem(Fight.getBlueTeam());
        packetDecoder[0xb1] = () -> pasteSchem(Fight.getRedTeam());
        packetDecoder[0xb2] = this::teams;
        packetDecoder[0xb3] = () -> pasteEmbeddedSchem(Fight.getBlueTeam());
        packetDecoder[0xb4] = () -> pasteEmbeddedSchem(Fight.getRedTeam());
        packetDecoder[0xc0] = this::scoreboardTitle;
        packetDecoder[0xc1] = this::scoreboardData;
        packetDecoder[0xc2] = this::bossBar;
        packetDecoder[0xc3] = this::subtitle;
        packetDecoder[0xc4] = this::printWin;
        packetDecoder[0xc5] = this::messageSubtitle;
        packetDecoder[0xc6] = this::winMessage;
        packetDecoder[0xc7] = this::bossBarMessage;
        packetDecoder[0xef] = source::readUTF;
        packetDecoder[0xff] = this::tick;

        execSync(FightWorld::forceLoad);

        if(source.async()) {
            Bukkit.getScheduler().runTaskAsynchronously(FightSystem.getPlugin(), this::process);
            task = Bukkit.getScheduler().runTaskTimer(FightSystem.getPlugin(), this::runSync, 1, 1);
        }else
            task = Bukkit.getScheduler().runTaskTimer(FightSystem.getPlugin(), this::process, 1, 1);

        Bukkit.getPluginManager().registerEvents(this, FightSystem.getPlugin());
        for(Player player : Bukkit.getOnlinePlayers())
            entityServer.addPlayer(player);
    }

    private void addREntity(int entityId, REntity entity) {
        entities.put(entityId, entity);
        FightSystem.getHullHider().updateREntity(entity);
    }

    @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
    public void onPlayerJoin(PlayerJoinEvent e) {
        entityServer.addPlayer(e.getPlayer());
    }

    public void skipToSubtitle() {
        skipToSubtitle = true;
    }

    private void winMessage() throws IOException {
        byte team = source.readByte();
        Message message = readMessage();

        execSync(() -> {
            FightTeam winner = null;
            if(team == 0x01)
                winner = Fight.getBlueTeam();
            else if(team == 0x02)
                winner = Fight.getRedTeam();

            FightSystem.setSpectateState(winner, "Replay ends", message.getMsg(), message.getParams());
        });
    }

    private void system() throws IOException {
        Message message = readMessage();
        FightSystem.getMessage().broadcast(message.getMsg(), message.getParams());
    }

    private void messageSubtitle() throws IOException {
        Message message = readMessage();
        FightUI.addSubtitle(message.getMsg(), message.getParams());
        skipToSubtitle = false;
    }

    private void chat() throws IOException {
        Message message = readMessage();
        FightSystem.getMessage().chat(message.getMsg(), message.getParams());
    }

    private void runSync() {
        synchronized (syncList) {
            for(Runnable runnable : syncList) {
                try{
                    runnable.run();
                }catch (Exception e) {
                    Bukkit.getLogger().log(Level.WARNING, "Failed to execute packet LastPacket: " + Arrays.toString(lastPackets.toArray()), e);
                }
            }
            syncList.clear();
        }
    }

    private void execSync(Runnable runnable){
        if (!source.async()) {
            runnable.run();
            return;
        }

        synchronized (syncList) {
            syncList.add(runnable);
        }
    }

    private void playerJoins() throws IOException {
        int entityId = source.readInt();
        int userId = source.readInt();

        execSync(() -> {
            SteamwarUser user = SteamwarUser.get(userId);
            addREntity(entityId, new RPlayer(entityServer, user.getUUID(), user.getUserName(), Config.SpecSpawn));
            team.addEntry(user.getUserName());

            if(ArenaMode.Test.contains(Config.mode))
                entities.get(entityId).setGlowing(true);
        });
    }

    private void entityMoves() throws IOException {
        int entityId = source.readInt();
        double x = source.readDouble() - arenaMinX;
        double locY = source.readDouble() - arenaMinY + Config.BluePasteRegion.getMinY();
        double z = source.readDouble() - arenaMinZ;
        if(rotateZ) {
            x = Config.ArenaRegion.getSizeX() - x;
            z = Config.ArenaRegion.getSizeZ() - z;
        }
        double locX = x + Config.ArenaRegion.getMinX();
        double locZ = z + Config.ArenaRegion.getMinZ();
        float pitch = source.readFloat();
        float yaw = source.readFloat() + (rotateZ ? 180 : 0);
        byte headYaw = (byte)((source.readByte() + (rotateZ ? 128 : 0)) % 256);

        execSync(() -> {
            REntity entity = entities.get(entityId);
            if(entity != null) {
                entity.move(locX, locY, locZ, pitch, yaw, headYaw);
                FightSystem.getHullHider().updateREntity(entity);
            }
        });
    }

    private void entityDespawns() throws IOException {
        int entityId = source.readInt();

        execSync(() -> {
            REntity entity = entities.remove(entityId);
            if(entity != null) {
                FightSystem.getHullHider().despawnREntity(entity);
                entity.die();
            }
        });
    }

    private void entitySneak() throws IOException {
        int entityId = source.readInt();
        boolean sneaking = source.readBoolean();

        execSync(() -> entities.get(entityId).setPose(sneaking ? de.steamwar.core.FlatteningWrapper.EntityPose.SNEAKING : de.steamwar.core.FlatteningWrapper.EntityPose.NORMAL));
    }

    private void entityAnimation() throws IOException {
        int entityId = source.readInt();
        byte animation = source.readByte();

        execSync(() -> entities.get(entityId).showAnimation(animation));
    }

    private void tntSpawn() throws IOException {
        int entityId = source.readInt();

        execSync(() -> addREntity(entityId, new REntity(entityServer, TrickyTrialsWrapper.impl.getTntEntityType(), Config.SpecSpawn)));
    }

    private void entityVelocity() throws IOException {
        int entityId = source.readInt();

        double dX = rotateZ ? -source.readDouble() : source.readDouble();
        double dY = source.readDouble();
        double dZ = rotateZ ? -source.readDouble() : source.readDouble();

        execSync(() -> {
            REntity entity = entities.get(entityId);
            if(entity != null)
                entity.setVelocity(dX, dY, dZ);
        });
    }

    private void playerItem() throws IOException {
        int entityId = source.readInt();
        String item = source.readUTF();
        boolean enchanted = source.readBoolean();
        String slotName = source.readUTF();

        ItemStack stack = new ItemStack(Material.valueOf(item.replace("minecraft:", "").toUpperCase()), 1);
        if(enchanted)
            stack.addUnsafeEnchantment(Enchantment.DURABILITY, 1);

        Object slot;
        switch(slotName){
            case "HEAD":
                slot = de.steamwar.core.ProtocolWrapper.itemSlots[5];
                break;
            case "CHEST":
                slot = de.steamwar.core.ProtocolWrapper.itemSlots[4];
                break;
            case "LEGS":
                slot = de.steamwar.core.ProtocolWrapper.itemSlots[3];
                break;
            case "FEET":
                slot = de.steamwar.core.ProtocolWrapper.itemSlots[2];
                break;
            case "OFFHAND":
                slot = de.steamwar.core.ProtocolWrapper.itemSlots[1];
                break;
            case "MAINHAND":
            default:
                slot = de.steamwar.core.ProtocolWrapper.itemSlots[0];
        }

        execSync(() -> entities.get(entityId).setItem(slot, stack));
    }

    private void arrowSpawn() throws IOException {
        int entityId = source.readInt();

        execSync(() -> addREntity(entityId, new REntity(entityServer, EntityType.ARROW, Config.SpecSpawn)));
    }

    private void fireballSpawn() throws IOException {
        int entityId = source.readInt();

        execSync(() -> addREntity(entityId, new REntity(entityServer, EntityType.FIREBALL, Config.SpecSpawn)));
    }

    private void send(ChatMessageType type) throws IOException {
        String message = source.readUTF();

        BaseComponent[] text = TextComponent.fromLegacyText(message);
        Bukkit.getOnlinePlayers().forEach(p -> de.steamwar.core.BountifulWrapper.impl.sendMessage(p, type, text));
    }

    private void countdown() throws IOException {
        String message = source.readUTF();
        int displaytime = source.readInt();
        Message appendix = readMessage();

        Bukkit.getOnlinePlayers().forEach(p -> Countdown.sendCountdownMessage(p, message, displaytime, appendix));
    }

    private void oldArenaInfo() throws IOException {
        rotateZ = source.readBoolean() != Config.blueNegZ();
        arenaMinY = Byte.toUnsignedInt(source.readByte());
        arenaMinX = source.readInt();
        arenaMinZ = source.readInt();
    }

    private void arenaInfo() throws IOException {
        rotateZ = source.readBoolean() != Config.blueNegZ();
        arenaMinX = source.readInt();
        arenaMinY = source.readInt();
        arenaMinZ = source.readInt();
    }

    private void shortBlock() throws IOException {
        int x = Byte.toUnsignedInt(source.readByte()) + Config.ArenaRegion.getMinX();
        int y = Byte.toUnsignedInt(source.readByte());
        int z = Byte.toUnsignedInt(source.readByte()) + Config.ArenaRegion.getMinZ();
        int blockState = source.readShort();

        setBlock(x, y, z, blockState);
    }

    private void shortRelativeBlock() throws IOException {
        int x = Byte.toUnsignedInt(source.readByte());
        int y = Byte.toUnsignedInt(source.readByte());
        int z = Byte.toUnsignedInt(source.readByte());
        int blockState = source.readShort();

        if(rotateZ) {
            x = Config.ArenaRegion.getSizeX() - x;
            z = Config.ArenaRegion.getSizeZ() - z;
        }

        setBlock(x + Config.ArenaRegion.getMinX(), y + Config.BluePasteRegion.getMinY(), z + Config.ArenaRegion.getMinZ(), blockState);
    }

    private void byteWorldHeightBlock() throws IOException {
        int x = source.readInt() - arenaMinX;
        int y = Byte.toUnsignedInt(source.readByte()) - arenaMinY;
        int z = source.readInt() - arenaMinZ;
        int blockState = source.readInt();

        if(rotateZ) {
            x = Config.ArenaRegion.getSizeX() - x;
            z = Config.ArenaRegion.getSizeZ() - z;
        }

        setBlock(x + Config.ArenaRegion.getMinX(), y + Config.BluePasteRegion.getMinY(), z + Config.ArenaRegion.getMinZ(), blockState);
    }

    private void block() throws IOException {
        int x = source.readInt() - arenaMinX;
        int y = source.readShort() - arenaMinY;
        int z = source.readInt() - arenaMinZ;
        int blockState = source.readInt();

        if(rotateZ) {
            x = Config.ArenaRegion.getSizeX() - x;
            z = Config.ArenaRegion.getSizeZ() - z;
        }

        setBlock(x + Config.ArenaRegion.getMinX(), y + Config.BluePasteRegion.getMinY(), z + Config.ArenaRegion.getMinZ(), blockState);
    }

    private void setBlock(int x, int y, int z, int blockState){
        if(!Config.ArenaRegion.in2dRegion(x, z))
            return; //Outside of the arena

        execSync(() -> {
            BlockIdWrapper.impl.setBlock(Config.world, x, y, z, TechHiderWrapper.ENABLED && hiddenBlockIds.contains(blockState) ? obfuscateWith : blockState);
            FightSystem.getHullHider().blockUpdate(Config.world.getBlockAt(x, y, z), BlockIdWrapper.impl.idToMaterial(blockState));
        });
    }

    private void particle() throws IOException {
        double x = source.readDouble() - arenaMinX;
        double y = source.readDouble() - arenaMinY;
        double z = source.readDouble() - arenaMinZ;
        String particleName = source.readUTF();

        if(rotateZ) {
            x = Config.ArenaRegion.getSizeX() - x;
            z = Config.ArenaRegion.getSizeZ() - z;
        }

        double finalX = x;
        double finalZ = z;
        execSync(() -> BountifulWrapper.impl.spawnParticle(Config.world, particleName, finalX + Config.ArenaRegion.getMinX(), y + Config.BluePasteRegion.getMinY(), finalZ + Config.ArenaRegion.getMinZ()));
    }

    private void sound() throws IOException {
        int rawX = source.readInt() - arenaMinX;
        int y = source.readInt() - arenaMinY + Config.BluePasteRegion.getMinY();
        int rawZ = source.readInt() - arenaMinZ;

        if(rotateZ) {
            rawX = Config.ArenaRegion.getSizeX() - rawX;
            rawZ = Config.ArenaRegion.getSizeZ() - rawZ;
        }

        int x = rawX + Config.ArenaRegion.getMinX();
        int z = rawZ + Config.ArenaRegion.getMinZ();

        String soundName = source.readUTF();
        String soundCategory = source.readUTF();

        float volume = source.readFloat();
        float pitch = source.readFloat();

        Sound sound = Sound.valueOf(soundName);

        execSync(() -> WorldOfColorWrapper.impl.playSound(new Location(Config.world, x, y, z), sound, soundCategory, volume, pitch));
    }

    private void soundAtPlayer() throws IOException {
        String soundName = source.readUTF();

        float volume = source.readFloat();
        float pitch = source.readFloat();

        Sound sound = Sound.valueOf(soundName);

        execSync(() -> Fight.playSound(sound, volume, pitch));
    }

    private void pasteSchem(FightTeam team) throws IOException {
        int schemId = source.readInt();
        if(schemId == 0)
            return;

        execSync(() -> team.pasteSchem(SchematicNode.getSchematicNode(schemId)));
    }

    private void pasteEmbeddedSchem(FightTeam team) throws IOException {
        int schemId = source.readInt();
        Clipboard clipboard = SchematicData.clipboardFromStream(new FilterInputStream(source) {
            @Override
            public void close() {
                // FAWE 1.12 calls close...
            }
        }, WorldEditWrapper.impl.getNativeFormat());

        execSync(() -> team.pasteSchem(schemId, clipboard));
    }

    private void teams() throws IOException {
        int blueId = source.readInt();
        int redId = source.readInt();

        execSync(() -> {
            pasteForTeam(blueId, Fight.getBlueTeam());
            pasteForTeam(redId, Fight.getRedTeam());
        });
    }

    private void pasteForTeam(int teamId, FightTeam fightTeam){
        Team team = Team.get(teamId);
        fightTeam.setPrefixAndName("§" + team.getTeamColor(), team.getTeamKuerzel());
        fightTeam.pasteTeamName();
    }

    private void scoreboardTitle() throws IOException {
        String title = source.readUTF();

        FightScoreboard.getScoreboard().setTitle(title);
    }

    private void scoreboardData() throws IOException {
        String key = source.readUTF();
        int value = source.readInt();

        FightScoreboard.getScoreboard().addScore(key, value);
    }

    private void bossBar() throws IOException {
        double leftBlueProgress = source.readDouble();
        double leftRedProgress = source.readDouble();
        String leftBlueText = source.readUTF();
        String leftRedText = source.readUTF();

        FightUI.getInstance().setBossbar(leftBlueProgress, leftRedProgress, leftBlueText, leftRedText);
    }

    private void bossBarMessage() throws IOException {
        double leftBlueProgress = source.readDouble();
        double leftRedProgress = source.readDouble();
        Message leftBlueText = readMessage();
        Message leftRedText = readMessage();

        FightUI.getInstance().setBossbar(leftBlueProgress, leftRedProgress, leftBlueText, leftRedText);
    }

    private void subtitle() throws IOException {
        String subtitle = source.readUTF();

        FightUI.addSubtitle("OLD_STRING", subtitle);
        skipToSubtitle = false;
    }

    private void printWin() throws IOException {
        String title = source.readUTF();
        String subtitle = source.readUTF();

        Bukkit.getOnlinePlayers().forEach(p -> {
            p.resetTitle();
            WorldOfColorWrapper.impl.sendTitle(p, title, subtitle, 5, 40, 5);
        });
    }

    private void endReplay() {
        HandlerList.unregisterAll(this);
        entityServer.close();
        entities.values().forEach(FightSystem.getHullHider()::despawnREntity);
        entities.clear();

        freezer.disable();
        if(!Config.replayserver()) {
            FightSystem.getMessage().broadcast("REPLAY_ENDS");
        }
        if (FightSystem.getLastWinner() != null) {
            FightUI.getInstance().setBossbar(0.5, 0.5,
                    new Message("BAR_WIN", FightSystem.getLastWinner().getColoredName(), Fight.getBlueTeam().getColoredName(), Fight.getRedTeam().getColoredName(), ""),
                    new Message("BAR_WIN", FightSystem.getLastWinner().getColoredName(), Fight.getRedTeam().getColoredName(), Fight.getBlueTeam().getColoredName(), ""));
        } else {
            FightUI.getInstance().setBossbar(0.5, 0.5,
                    new Message("BAR_TIE", "", Fight.getBlueTeam().getColoredName(), Fight.getRedTeam().getColoredName()),
                    new Message("BAR_TIE", "", Fight.getRedTeam().getColoredName(), Fight.getBlueTeam().getColoredName()));
        }
        FightState.setFightState(FightState.SPECTATE);
        currentProcessor = null;
        task.cancel();
    }

    private void bow() throws IOException {
        int entityId = source.readInt();
        boolean drawn = source.readBoolean();
        boolean offHand = source.readBoolean();

        execSync(() -> entities.get(entityId).setBowDrawn(drawn, offHand));
    }

    private void damage() throws IOException {
        int entityId = source.readInt();

        execSync(() -> entities.get(entityId).showDamage());
    }

    private void fireTick() throws IOException {
        int entityId = source.readInt();
        boolean perma = source.readBoolean();

        execSync(() -> entities.get(entityId).setOnFire(perma));
    }

    private void tick(){
        execSync(entityServer::tick);

        if(!source.async() && !skipToSubtitle)
            tickFinished = true;
    }

    private void process(){
        tickFinished = false;
        try{
            while(!source.isClosed() && !tickFinished){
                int packetType = Byte.toUnsignedInt(source.readByte());

                lastPackets.add(packetType);
                if (lastPackets.size() > 10)
                    lastPackets.remove(0);

                PacketParser parser = packetDecoder[packetType];
                if(parser != null){
                    parser.process();
                }else{
                    Bukkit.getLogger().log(Level.SEVERE, "Unknown packet " + packetType + " recieved, closing. LastPacket: " + Arrays.toString(lastPackets.toArray()));
                    source.close();
                }
            }
        } catch (EOFException e) {
            Bukkit.getLogger().log(Level.INFO, "The FightServer is offline");
            source.close();
        } catch(Exception e) {
            Bukkit.getLogger().log(Level.WARNING, "Could not recieve packet, LastPacket: " + Arrays.toString(lastPackets.toArray()), e);
            source.close();
        }

        if(source.isClosed()){
            execSync(() -> Bukkit.getScheduler().runTask(FightSystem.getPlugin(), this::endReplay));
        }
    }

    private Message readMessage() throws IOException {
        String msg = source.readUTF();
        List<Object> params = new ArrayList<>();

        while(true) {
            int type = source.readUnsignedByte();
            switch(type) {
                case 0x00:
                    return new Message(msg, params.toArray());
                case 0x01:
                    params.add(source.readBoolean());
                    break;
                case 0x02:
                    params.add(source.readByte());
                    break;
                case 0x03:
                    params.add(source.readShort());
                    break;
                case 0x04:
                    params.add(source.readInt());
                    break;
                case 0x05:
                    params.add(source.readFloat());
                    break;
                case 0x06:
                    params.add(source.readDouble());
                    break;
                case 0x07:
                    params.add(source.readUTF());
                    break;
                case 0x08:
                    params.add(readMessage());
                    break;
                default:
                    throw new IOException("Unknown message param type " + type);
            }
        }
    }

    private interface PacketParser{
        void process() throws IOException;
    }
}
