package org.allaymc.server.gui;

import com.formdev.flatlaf.themes.FlatMacDarkLaf;
import com.intellij.uiDesigner.core.GridConstraints;
import com.intellij.uiDesigner.core.GridLayoutManager;
import org.allaymc.api.eventbus.EventHandler;
import org.allaymc.api.eventbus.event.server.PlayerJoinEvent;
import org.allaymc.api.eventbus.event.server.PlayerQuitEvent;
import org.allaymc.api.message.I18n;
import org.allaymc.api.message.TrKeys;
import org.allaymc.api.plugin.PluginDependency;
import org.allaymc.api.registry.Registries;
import org.allaymc.api.server.Server;
import org.allaymc.api.utils.AllayStringUtils;
import org.allaymc.server.command.AllayCommandSuggester;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.core.Layout;
import org.apache.logging.log4j.core.LogEvent;
import org.apache.logging.log4j.core.LoggerContext;
import org.apache.logging.log4j.core.appender.AbstractAppender;
import org.apache.logging.log4j.core.config.Property;
import org.apache.logging.log4j.core.layout.PatternLayout;
import org.jetbrains.annotations.NotNull;

import javax.swing.*;
import javax.swing.event.DocumentEvent;
import javax.swing.event.DocumentListener;
import javax.swing.table.DefaultTableModel;
import javax.swing.text.Document;
import java.awt.*;
import java.awt.event.*;
import java.io.OutputStream;
import java.io.PrintStream;
import java.io.Serializable;
import java.net.URL;
import java.util.*;
import java.util.List;
import java.util.stream.Collectors;

/**
 * @author daoge_cmd
 */
public final class Dashboard {
    public static final int RAM_VALUE_COUNT = 100;
    public static final int CHUNK_VALUE_COUNT = 100;
    public static final int ENTITY_VALUE_COUNT = 100;
    private static final long MEGABYTE = 1024L * 1024L;
    private static final String LOG_APPENDER_NAME = "Dashboard";
    private static final String CONSOLE_APPENDER_NAME = "TerminalConsole";
    private static final String LOG_PATTERN = "[%cyan{%d{HH:mm:ss} %level}] [%yellow{%t}] [%blue{%logger{0}}] %minecraftFormatting{%msg}%n";
    private static final Map<Integer, Color> DIMENSION_COLORS = Map.of(
            0, new Color(76, 175, 80, 200),
            1, new Color(244, 67, 54, 200),
            2, new Color(156, 39, 176, 200)
    );
    private static final Map<Integer, String> DIMENSION_NAMES = Map.of(
            0, "Overworld",
            1, "Nether",
            2, "The End"
    );
    private static Dashboard INSTANCE;
    private JPanel rootPane;
    private JTabbedPane rootTabbedPane;
    private JPanel playerTab;
    private JPanel perfTab;
    private JTable playerTable;
    private JTable pluginTable;
    private JLabel onlinePlayerCount;
    private GraphPanel ramGraph;
    private GraphPanel chunkGraph;
    private GraphPanel entityGraph;
    private ConsolePanel consolePane;
    private JTextField cmdInput;
    private JPanel entityTab;
    private JPanel chunkTab;
    private JPanel memoryTab;
    private JPanel consoleTab;
    private JPanel pluginTab;
    private JTabbedPane perfTabbedPane;
    private List<Integer> ramValues;
    private CommandAutocomplete commandAutocomplete;
    private final Map<String, Map<Integer, List<Integer>>> chunkCountByWorld = new LinkedHashMap<>();
    private final Map<String, Map<Integer, List<Integer>>> entityCountByWorld = new LinkedHashMap<>();
    private String selectedWorld;

    private Dashboard() {
        $$$setupUI$$$();
        wrapSystemOutputStreams();
        attachLogAppender();
        JFrame frame = new JFrame(I18n.get().tr(TrKeys.ALLAY_GUI_NAME));
        frame.setContentPane(rootPane);
        frame.setDefaultCloseOperation(WindowConstants.DO_NOTHING_ON_CLOSE);
        frame.addWindowListener(new WindowAdapter() {
            @Override
            public void windowClosing(WindowEvent e) {
                Server.getInstance().shutdown();
            }
        });
        frame.setSize(700, 700);
        frame.setLocationRelativeTo(null);
        // Set icon
        URL image = Dashboard.class.getClassLoader().getResource("icon.png");
        if (image != null) {
            frame.setIconImage(new ImageIcon(image).getImage());
        }
        applyI18nTexts();
        // Show the frame
        frame.setVisible(true);
        pluginTable.addMouseListener(new MouseAdapter() {
            @Override
            public void mouseClicked(MouseEvent e) {
                if (e.getButton() != MouseEvent.BUTTON3) return;
                // Show the focused plugin's information
                JPopupMenu popupMenu = new JPopupMenu();
                JMenuItem infoItem = new JMenuItem(I18n.get().tr(TrKeys.ALLAY_GUI_INFO));
                infoItem.addActionListener($ -> {
                    if (pluginTable.getSelectedRow() == -1) return;
                    // Get the plugin
                    String pluginName = (String) pluginTable.getValueAt(pluginTable.getSelectedRow(), 0);
                    var pluginDescriptor = Server.getInstance().getPluginManager().getEnabledPlugin(pluginName).descriptor();
                    JOptionPane.showMessageDialog(null,
                            I18n.get().tr(TrKeys.ALLAY_GUI_PLUGIN_NAME) + ": " + pluginDescriptor.getName() + "\n" +
                            I18n.get().tr(TrKeys.ALLAY_GUI_PLUGIN_ENTRANCE) + ": " + pluginDescriptor.getEntrance() + "\n" +
                            I18n.get().tr(TrKeys.ALLAY_GUI_PLUGIN_DESCRIPTION) + ": " + pluginDescriptor.getDescription() + "\n" +
                            I18n.get().tr(TrKeys.ALLAY_GUI_PLUGIN_VERSION) + ": " + pluginDescriptor.getVersion() + "\n" +
                            I18n.get().tr(TrKeys.ALLAY_GUI_PLUGIN_API_VERSION) + ": " + pluginDescriptor.getAPIVersion() + "\n" +
                            I18n.get().tr(TrKeys.ALLAY_GUI_PLUGIN_AUTHORS) + ": " + String.join(", ", pluginDescriptor.getAuthors()) + "\n" +
                            I18n.get().tr(TrKeys.ALLAY_GUI_PLUGIN_DEPENDENCIES) + ": " + pluginDescriptor.getDependencies().stream().map(PluginDependency::name).collect(Collectors.joining(", ")) + "\n" +
                            I18n.get().tr(TrKeys.ALLAY_GUI_PLUGIN_WEBSITE) + ": " + pluginDescriptor.getWebsite(),
                            I18n.get().tr(TrKeys.ALLAY_GUI_PLUGIN_INFO),
                            JOptionPane.INFORMATION_MESSAGE);
                });
                popupMenu.add(infoItem);
                popupMenu.show(e.getComponent(), e.getX(), e.getY());
            }
        });
        playerTable.addMouseListener(new MouseAdapter() {
            @Override
            public void mouseClicked(MouseEvent e) {
                if (e.getButton() != MouseEvent.BUTTON3) return;
                JPopupMenu popupMenu = new JPopupMenu();

                JMenuItem infoItem = new JMenuItem(I18n.get().tr(TrKeys.ALLAY_GUI_INFO));
                infoItem.addActionListener($ -> {
                    if (playerTable.getSelectedRow() == -1) return;
                    // Get the player
                    String playerName = (String) playerTable.getValueAt(playerTable.getSelectedRow(), 0);
                    var player = Server.getInstance().getPlayerManager().getPlayerByName(playerName);
                    var pos = player.getControlledEntity().getLocation();
                    JOptionPane.showMessageDialog(null,
                            I18n.get().tr(TrKeys.ALLAY_GUI_PLAYER_NAME) + ": " + player.getOriginName() + "\n" +
                            I18n.get().tr(TrKeys.ALLAY_GUI_PLAYER_ADDRESS) + ": " + player.getSocketAddress().toString() + "\n" +
                            I18n.get().tr(TrKeys.ALLAY_GUI_PLAYER_UUID) + ": " + player.getLoginData().getUuid().toString() + "\n" +
                            I18n.get().tr(TrKeys.ALLAY_GUI_PLAYER_POS) + ": (" + pos.x() + ", " + pos.y() + ", " + pos.z() + ")" + "\n" +
                            I18n.get().tr(TrKeys.ALLAY_GUI_PLAYER_WORLD) + ": " + pos.dimension().getWorld().getWorldData().getDisplayName() + "\n" +
                            I18n.get().tr(TrKeys.ALLAY_GUI_PLAYER_DIMENSION) + ": " + pos.dimension().getDimensionType().getIdentifier(),
                            I18n.get().tr(TrKeys.ALLAY_GUI_PLAYER_INFO),
                            JOptionPane.INFORMATION_MESSAGE);
                });
                popupMenu.add(infoItem);

                JMenuItem kickItem = new JMenuItem(I18n.get().tr(TrKeys.ALLAY_GUI_PLAYER_KICK));
                kickItem.addActionListener($ -> {
                    if (playerTable.getSelectedRow() == -1) return;
                    // Get the player
                    String playerName = (String) playerTable.getValueAt(playerTable.getSelectedRow(), 0);
                    var player = Server.getInstance().getPlayerManager().getPlayerByName(playerName);
                    player.disconnect(TrKeys.MC_DISCONNECT_DISCONNECTED);
                });
                popupMenu.add(kickItem);

                JMenuItem banItem = new JMenuItem(I18n.get().tr(TrKeys.ALLAY_GUI_PLAYER_BAN));
                banItem.addActionListener($ -> {
                    if (playerTable.getSelectedRow() == -1) return;
                    // Get the player
                    String playerName = (String) playerTable.getValueAt(playerTable.getSelectedRow(), 0);
                    var player = Server.getInstance().getPlayerManager().getPlayerByName(playerName);
                    Server.getInstance().getPlayerManager().ban(player.getLoginData().getUuid().toString());
                });
                popupMenu.add(banItem);

                JMenuItem banIpItem = new JMenuItem(I18n.get().tr(TrKeys.ALLAY_GUI_PLAYER_BANIP));
                banIpItem.addActionListener($ -> {
                    if (playerTable.getSelectedRow() == -1) return;
                    // Get the player
                    String playerName = (String) playerTable.getValueAt(playerTable.getSelectedRow(), 0);
                    var player = Server.getInstance().getPlayerManager().getPlayerByName(playerName);
                    Server.getInstance().getPlayerManager().banIP(AllayStringUtils.fastTwoPartSplit(player.getSocketAddress().toString().substring(1), ":", "")[0]);
                });
                popupMenu.add(banIpItem);

                var manager = Server.getInstance().getPlayerManager();
                int selectedRow = playerTable.getSelectedRow();
                if (selectedRow != -1) {
                    String selectedPlayerName = (String) playerTable.getValueAt(selectedRow, 0);
                    var selectedPlayer = manager.getPlayerByName(selectedPlayerName);
                    if (selectedPlayer != null) {
                        boolean isOp = manager.isOperator(selectedPlayer);
                        JMenuItem opItem = new JMenuItem(I18n.get().tr(isOp ? TrKeys.ALLAY_GUI_PLAYER_DEOP : TrKeys.ALLAY_GUI_PLAYER_OP));
                        opItem.addActionListener($ -> {
                            manager.setOperator(selectedPlayer.getOriginName(), !isOp);
                            selectedPlayer.sendTranslatable(isOp ? TrKeys.MC_COMMANDS_DEOP_MESSAGE : TrKeys.MC_COMMANDS_OP_MESSAGE);
                        });
                        popupMenu.add(opItem);
                    }
                }

                popupMenu.show(e.getComponent(), e.getX(), e.getY());
            }
        });
        consolePane.addMouseListener(new MouseAdapter() {
            @Override
            public void mouseClicked(MouseEvent e) {
                if (e.getButton() != MouseEvent.BUTTON3) return;
                JPopupMenu popupMenu = new JPopupMenu();

                JMenuItem changeFontSizeItem = new JMenuItem(I18n.get().tr(TrKeys.ALLAY_GUI_CONSOLE_CHANGEFONTSIZE));
                changeFontSizeItem.addActionListener($ -> {
                    String input = JOptionPane.showInputDialog(I18n.get().tr(TrKeys.ALLAY_GUI_CONSOLE_CHANGEFONTSIZE_DIALOG), consolePane.getFont().getSize());
                    if (input == null) return;
                    try {
                        int fontSize = Integer.parseInt(input);
                        consolePane.setFont(consolePane.getFont().deriveFont((float) fontSize));
                    } catch (NumberFormatException ex) {
                        JOptionPane.showMessageDialog(null, I18n.get().tr(TrKeys.ALLAY_GUI_CONSOLE_CHANGEFONTSIZE_ERROR_DIALOG), I18n.get().tr(TrKeys.ALLAY_GUI_ERROR), JOptionPane.ERROR_MESSAGE);
                    }
                });
                popupMenu.add(changeFontSizeItem);

                JMenuItem clearItem = new JMenuItem(I18n.get().tr(TrKeys.ALLAY_GUI_CONSOLE_CLEAR));
                clearItem.addActionListener($ -> consolePane.setText(""));
                popupMenu.add(clearItem);

                popupMenu.show(e.getComponent(), e.getX(), e.getY());
            }
        });
        var worldSelectListener = new MouseAdapter() {
            @Override
            public void mouseClicked(MouseEvent e) {
                if (e.getButton() != MouseEvent.BUTTON3) return;
                var worldNames = new ArrayList<>(chunkCountByWorld.keySet());
                if (worldNames.isEmpty()) return;
                JPopupMenu popupMenu = new JPopupMenu();
                for (var worldName : worldNames) {
                    var item = new JCheckBoxMenuItem(worldName, worldName.equals(selectedWorld));
                    item.addActionListener($ -> {
                        selectedWorld = worldName;
                        refreshDimensionGraphs();
                    });
                    popupMenu.add(item);
                }
                popupMenu.show(e.getComponent(), e.getX(), e.getY());
            }
        };
        chunkGraph.addMouseListener(worldSelectListener);
        entityGraph.addMouseListener(worldSelectListener);
    }

    public static Dashboard getInstance() {
        if (INSTANCE == null) {
            FlatMacDarkLaf.setup();
            INSTANCE = new Dashboard();
        }
        return INSTANCE;
    }

    private void applyI18nTexts() {
        SwingUtilities.invokeLater(() -> {
            rootTabbedPane.setTitleAt(0, I18n.get().tr(TrKeys.ALLAY_GUI_CONSOLE));
            rootTabbedPane.setTitleAt(1, I18n.get().tr(TrKeys.ALLAY_GUI_PERFORMANCE));
            rootTabbedPane.setTitleAt(2, I18n.get().tr(TrKeys.ALLAY_GUI_PLAYER));
            rootTabbedPane.setTitleAt(3, I18n.get().tr(TrKeys.ALLAY_GUI_PLUGIN));

            perfTabbedPane.setTitleAt(0, (I18n.get().tr(TrKeys.ALLAY_GUI_MEMORY)));
            perfTabbedPane.setTitleAt(1, (I18n.get().tr(TrKeys.ALLAY_GUI_CHUNK)));
            perfTabbedPane.setTitleAt(2, (I18n.get().tr(TrKeys.ALLAY_GUI_ENTITY)));

            onlinePlayerCount.setText(I18n.get().tr(TrKeys.ALLAY_GUI_PLAYER_ONLINE, 0));
        });
    }

    private void wrapSystemOutputStreams() {
        var proxyOutputStream = createProxyOutputStream();
        // Override the system output streams
        System.setOut(new PrintStream(proxyOutputStream, true));
        System.setErr(new PrintStream(proxyOutputStream, true));
    }

    private void attachLogAppender() {
        var ctx = (LoggerContext) LogManager.getContext(false);
        var log4jConfig = ctx.getConfiguration();
        if (log4jConfig.getAppender(LOG_APPENDER_NAME) != null) return;

        Layout<? extends Serializable> layout = null;
        var consoleAppender = log4jConfig.getAppender(CONSOLE_APPENDER_NAME);
        if (consoleAppender != null) {
            layout = consoleAppender.getLayout();
        }
        if (layout == null) {
            layout = PatternLayout.newBuilder()
                    .withConfiguration(log4jConfig)
                    .withPattern(LOG_PATTERN)
                    .build();
        }

        var appender = new DashboardLogAppender(LOG_APPENDER_NAME, layout, this);
        appender.start();
        log4jConfig.addAppender(appender);
        log4jConfig.getLoggerConfig(LogManager.ROOT_LOGGER_NAME).addAppender(appender, null, null);
        ctx.updateLoggers();
    }

    private @NotNull OutputStream createProxyOutputStream() {
        var originalOutputStream = System.out;
        return new OutputStream() {
            @Override
            public void write(int i) {
                originalOutputStream.write(i);
                appendTextToConsole(String.valueOf((char) i));
            }

            @Override
            public void write(byte @NotNull [] b) {
                write(b, 0, b.length);
            }

            @Override
            public void write(byte @NotNull [] b, int off, int len) {
                originalOutputStream.write(b, off, len);
                appendTextToConsole(new String(b, off, len));
            }
        };
    }

    public void afterServerStarted() {
        Runnable guiUpdateTask = () -> {
            // Update ram graph
            final long freeMemory = Runtime.getRuntime().freeMemory();
            final long totalMemory = Runtime.getRuntime().totalMemory();
            final int freePercent = (int) (freeMemory * 100.0 / totalMemory + 0.5);
            ramValues.add(100 - freePercent);
            ramGraph.setXLabel(I18n.get().tr(TrKeys.ALLAY_GUI_RAM_LABEL, String.format("%,d", (totalMemory - freeMemory) / MEGABYTE), freePercent));
            // Trim the list
            int k = ramValues.size();
            if (k > RAM_VALUE_COUNT)
                ramValues.subList(0, k - RAM_VALUE_COUNT).clear();
            // Update the graph
            ramGraph.setValues(ramValues);

            // Update chunk & entity graphs - per world per dimension
            var loadedWorldNames = new HashSet<String>();
            Server.getInstance().getWorldPool().getWorlds().values().forEach(world -> {
                var worldName = world.getWorldData().getDisplayName();
                loadedWorldNames.add(worldName);
                var chunkDimMap = chunkCountByWorld.computeIfAbsent(worldName, n -> new LinkedHashMap<>());
                var entityDimMap = entityCountByWorld.computeIfAbsent(worldName, n -> new LinkedHashMap<>());
                var loadedDimIds = world.getDimensions().keySet();
                // Remove dimensions that no longer exist in this world
                chunkDimMap.keySet().retainAll(loadedDimIds);
                entityDimMap.keySet().retainAll(loadedDimIds);
                // Append data for existing dimensions
                world.getDimensions().values().forEach(dimension -> {
                    int dimId = dimension.getDimensionType().getId();
                    appendAndTrim(
                            chunkDimMap.computeIfAbsent(dimId, id -> new ArrayList<>(Collections.nCopies(CHUNK_VALUE_COUNT, 0))),
                            dimension.getChunkManager().getLoadedChunks().size(), CHUNK_VALUE_COUNT);
                    appendAndTrim(
                            entityDimMap.computeIfAbsent(dimId, id -> new ArrayList<>(Collections.nCopies(ENTITY_VALUE_COUNT, 0))),
                            dimension.getEntityCount(), ENTITY_VALUE_COUNT);
                });
            });

            // Remove unloaded worlds
            chunkCountByWorld.keySet().retainAll(loadedWorldNames);
            entityCountByWorld.keySet().retainAll(loadedWorldNames);

            // Auto-select first world if none selected or selected world was unloaded
            if (selectedWorld == null || !chunkCountByWorld.containsKey(selectedWorld)) {
                selectedWorld = chunkCountByWorld.keySet().stream().findFirst().orElse(null);
            }

            refreshDimensionGraphs();
        };

        // SwingUtilities.invokeLater is called so that we don't run into threading issues with the GUI
        Server.getInstance().getScheduler().scheduleRepeating(
                Server.getInstance(),
                () -> {
                    SwingUtilities.invokeLater(guiUpdateTask);
                    return true;
                },
                20);

        var server = Server.getInstance();
        server.getEventBus().registerListener(this);
        updatePluginTable();

        cmdInput.addActionListener(new CommandListener());
        commandAutocomplete = new CommandAutocomplete(this.cmdInput, new AllayCommandSuggester(server));
        SwingUtilities.invokeLater(() -> {
            cmdInput.setEnabled(true);
            cmdInput.requestFocusInWindow();
        });
    }

    @EventHandler
    private void onPlayerJoin(PlayerJoinEvent event) {
        updateOnlinePlayerCount();
        updateOnlinePlayerTable();
    }

    @EventHandler
    private void onPlayerQuit(PlayerQuitEvent event) {
        updateOnlinePlayerCount();
        updateOnlinePlayerTable();
    }

    private void updateOnlinePlayerCount() {
        SwingUtilities.invokeLater(() -> onlinePlayerCount.setText(I18n.get().tr(TrKeys.ALLAY_GUI_PLAYER_ONLINE, Server.getInstance().getPlayerManager().getPlayerCount())));
    }

    private void updateOnlinePlayerTable() {
        var title = new String[]{I18n.get().tr(TrKeys.ALLAY_GUI_PLAYER_NAME), I18n.get().tr(TrKeys.ALLAY_GUI_PLAYER_ADDRESS), I18n.get().tr(TrKeys.ALLAY_GUI_PLAYER_UUID)};
        var players = Server.getInstance().getPlayerManager().getPlayers().values();
        String[][] data = new String[players.size()][3];
        int row = 0;
        for (var player : players) {
            data[row] = new String[]{
                    player.getOriginName(),
                    player.getSocketAddress().toString(),
                    player.getLoginData().getUuid().toString()
            };
            row++;
        }
        var model = new UneditableDefaultTableModel(data, title);
        SwingUtilities.invokeLater(() -> {
            playerTable.setModel(model);
            playerTable.getTableHeader().setReorderingAllowed(false);
        });
    }

    private static void appendAndTrim(List<Integer> list, int value, int maxSize) {
        list.add(value);
        int sz = list.size();
        if (sz > maxSize) list.subList(0, sz - maxSize).clear();
    }

    private void refreshDimensionGraphs() {
        if (selectedWorld == null) return;
        var chunkDimMap = chunkCountByWorld.get(selectedWorld);
        var entityDimMap = entityCountByWorld.get(selectedWorld);
        if (chunkDimMap == null || entityDimMap == null) return;

        var chunkSeries = buildDimSeriesList(chunkDimMap);
        var totalChunks = chunkDimMap.values().stream()
                .mapToInt(list -> list.isEmpty() ? 0 : list.getLast())
                .sum();
        chunkGraph.setXLabel(I18n.get().tr(TrKeys.ALLAY_GUI_CHUNK_LABEL, totalChunks));
        chunkGraph.setSeries(chunkSeries);

        var entitySeries = buildDimSeriesList(entityDimMap);
        var totalEntities = entityDimMap.values().stream()
                .mapToInt(list -> list.isEmpty() ? 0 : list.getLast())
                .sum();
        entityGraph.setXLabel(I18n.get().tr(TrKeys.ALLAY_GUI_ENTITY_LABEL, totalEntities));
        entityGraph.setSeries(entitySeries);
    }

    private static List<GraphPanel.Series> buildDimSeriesList(Map<Integer, List<Integer>> dimMap) {
        var series = new ArrayList<GraphPanel.Series>();
        dimMap.entrySet().stream()
                .sorted(Map.Entry.comparingByKey())
                .forEach(entry -> series.add(new GraphPanel.Series(
                        DIMENSION_NAMES.getOrDefault(entry.getKey(), "Dim " + entry.getKey()),
                        DIMENSION_COLORS.getOrDefault(entry.getKey(), Color.WHITE),
                        new ArrayList<>(entry.getValue())
                )));
        return series;
    }

    private void updatePluginTable() {
        var title = new String[]{I18n.get().tr(TrKeys.ALLAY_GUI_PLUGIN_NAME), I18n.get().tr(TrKeys.ALLAY_GUI_PLUGIN_DESCRIPTION), I18n.get().tr(TrKeys.ALLAY_GUI_PLUGIN_VERSION), I18n.get().tr(TrKeys.ALLAY_GUI_PLUGIN_AUTHORS)};
        var plugins = Server.getInstance().getPluginManager().getEnabledPlugins().values();
        var data = new String[plugins.size()][4];
        int row = 0;
        for (var plugin : plugins) {
            var descriptor = plugin.descriptor();
            data[row] = new String[]{
                    descriptor.getName(),
                    descriptor.getDescription().isBlank() ? "N/A" : descriptor.getDescription(),
                    descriptor.getVersion(),
                    String.join(", ", descriptor.getAuthors())
            };
            row++;
        }
        var model = new UneditableDefaultTableModel(data, title);
        SwingUtilities.invokeLater(() -> {
            pluginTable.setModel(model);
            pluginTable.getTableHeader().setReorderingAllowed(false);
        });
    }

    /**
     * Method generated by IntelliJ IDEA GUI Designer
     * >>> IMPORTANT!! <<<
     * DO NOT edit this method OR call it in your code!
     */
    private void $$$setupUI$$$() {
        createUIComponents();
        rootPane = new JPanel();
        rootPane.setLayout(new GridLayoutManager(1, 1, new Insets(8, 8, 8, 8), -1, -1));
        rootTabbedPane = new JTabbedPane();
        rootPane.add(rootTabbedPane, new GridConstraints(0, 0, 1, 1, GridConstraints.ANCHOR_CENTER, GridConstraints.FILL_BOTH, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW, null, new Dimension(200, 200), null, 0, false));
        consoleTab = new JPanel();
        consoleTab.setLayout(new GridLayoutManager(2, 1, new Insets(0, 0, 0, 0), -1, -1));
        rootTabbedPane.addTab("Console", consoleTab);
        final JScrollPane scrollPane1 = new JScrollPane();
        consoleTab.add(scrollPane1, new GridConstraints(0, 0, 1, 1, GridConstraints.ANCHOR_CENTER, GridConstraints.FILL_BOTH, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_WANT_GROW, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_WANT_GROW, null, null, null, 0, false));
        scrollPane1.setViewportView(consolePane);
        cmdInput = new JTextField();
        cmdInput.setEnabled(false);
        consoleTab.add(cmdInput, new GridConstraints(1, 0, 1, 1, GridConstraints.ANCHOR_WEST, GridConstraints.FILL_HORIZONTAL, GridConstraints.SIZEPOLICY_WANT_GROW, GridConstraints.SIZEPOLICY_FIXED, null, new Dimension(150, -1), null, 0, false));
        perfTab = new JPanel();
        perfTab.setLayout(new GridLayoutManager(1, 1, new Insets(0, 0, 0, 0), -1, -1));
        rootTabbedPane.addTab("Performance", perfTab);
        perfTabbedPane = new JTabbedPane();
        perfTabbedPane.setAlignmentY(0.5f);
        perfTab.add(perfTabbedPane, new GridConstraints(0, 0, 1, 1, GridConstraints.ANCHOR_CENTER, GridConstraints.FILL_BOTH, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW, null, new Dimension(200, 200), null, 0, false));
        memoryTab = new JPanel();
        memoryTab.setLayout(new GridLayoutManager(1, 1, new Insets(0, 0, 0, 0), -1, -1));
        perfTabbedPane.addTab("Memory", memoryTab);
        memoryTab.add(ramGraph, new GridConstraints(0, 0, 1, 1, GridConstraints.ANCHOR_CENTER, GridConstraints.FILL_NONE, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW, null, null, null, 0, false));
        chunkTab = new JPanel();
        chunkTab.setLayout(new GridLayoutManager(1, 1, new Insets(0, 0, 0, 0), -1, -1));
        perfTabbedPane.addTab("Chunk", chunkTab);
        chunkTab.add(chunkGraph, new GridConstraints(0, 0, 1, 1, GridConstraints.ANCHOR_CENTER, GridConstraints.FILL_NONE, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW, null, null, null, 0, false));
        entityTab = new JPanel();
        entityTab.setLayout(new GridLayoutManager(1, 1, new Insets(0, 0, 0, 0), -1, -1));
        perfTabbedPane.addTab("Entity", entityTab);
        entityTab.add(entityGraph, new GridConstraints(0, 0, 1, 1, GridConstraints.ANCHOR_CENTER, GridConstraints.FILL_NONE, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW, null, null, null, 0, false));
        playerTab = new JPanel();
        playerTab.setLayout(new GridLayoutManager(2, 1, new Insets(0, 0, 0, 0), -1, -1));
        rootTabbedPane.addTab("Players", playerTab);
        onlinePlayerCount = new JLabel();
        onlinePlayerCount.setEnabled(true);
        onlinePlayerCount.setHorizontalAlignment(2);
        onlinePlayerCount.setText("Online: 0");
        playerTab.add(onlinePlayerCount, new GridConstraints(0, 0, 1, 1, GridConstraints.ANCHOR_NORTH, GridConstraints.FILL_HORIZONTAL, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW, null, null, null, 0, false));
        final JScrollPane scrollPane2 = new JScrollPane();
        playerTab.add(scrollPane2, new GridConstraints(1, 0, 1, 1, GridConstraints.ANCHOR_CENTER, GridConstraints.FILL_BOTH, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_WANT_GROW, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_WANT_GROW, null, null, null, 0, false));
        playerTable = new JTable();
        playerTable.setAutoCreateRowSorter(true);
        scrollPane2.setViewportView(playerTable);
        pluginTab = new JPanel();
        pluginTab.setLayout(new GridLayoutManager(1, 1, new Insets(0, 0, 0, 0), -1, -1));
        rootTabbedPane.addTab("Plugins", pluginTab);
        final JScrollPane scrollPane3 = new JScrollPane();
        pluginTab.add(scrollPane3, new GridConstraints(0, 0, 1, 1, GridConstraints.ANCHOR_CENTER, GridConstraints.FILL_BOTH, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_WANT_GROW, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_WANT_GROW, null, null, null, 0, false));
        pluginTable = new JTable();
        pluginTable.setCellSelectionEnabled(false);
        scrollPane3.setViewportView(pluginTable);
    }

    /**
     *
     */
    public JComponent $$$getRootComponent$$$() {
        return rootPane;
    }

    private void createUIComponents() {
        // Init the three graph
        ramGraph = new GraphPanel();
        ramGraph.setVariableExtreme(false);
        ramValues = new ArrayList<>();
        // Set the ram graph to 0
        for (int i = 0; i < RAM_VALUE_COUNT; i++) {
            ramValues.add(0);
        }
        ramGraph.setValues(ramValues);

        chunkGraph = new GraphPanel();
        chunkGraph.setValues(Collections.nCopies(CHUNK_VALUE_COUNT, 0));

        entityGraph = new GraphPanel();
        entityGraph.setValues(Collections.nCopies(ENTITY_VALUE_COUNT, 0));

        // Init the console
        consolePane = new ConsolePanel();
        consolePane.setBackground(new Color(0x131313));
        consolePane.setEditable(false);
    }

    public void appendTextToConsole(final String text) {
        SwingUtilities.invokeLater(() -> {
            consolePane.appendANSI(text);
            Document doc = consolePane.getDocument();
            consolePane.setCaretPosition(doc.getLength());
        });
    }

    private static class UneditableDefaultTableModel extends DefaultTableModel {
        public UneditableDefaultTableModel(String[][] data, String[] title) {
            super(data, title);
        }

        @Override
        public boolean isCellEditable(int row, int column) {
            return false;
        }
    }

    private static final class CommandAutocomplete {
        private final JTextField input;
        private final AllayCommandSuggester suggester;
        private final JPopupMenu popup = new JPopupMenu();
        private final DefaultListModel<String> suggestionModel = new DefaultListModel<>();
        private final JList<String> suggestionList = new JList<>(suggestionModel);
        private boolean completing;

        private CommandAutocomplete(JTextField input, AllayCommandSuggester suggester) {
            this.input = input;
            this.suggester = suggester;
            this.setupPopup();
            this.setupBindings();
        }

        private void setupPopup() {
            this.suggestionList.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
            this.suggestionList.setVisibleRowCount(8);
            this.popup.setFocusable(false);
            this.popup.add(new JScrollPane(suggestionList));
        }

        private void setupBindings() {
            this.input.setFocusTraversalKeysEnabled(false);
            this.input.getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_TAB, 0), "complete-command");
            this.input.getActionMap().put("complete-command", new AbstractAction() {
                @Override
                public void actionPerformed(ActionEvent e) {
                    completeNext();
                }
            });
            this.input.getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0), "hide-command-completions");
            this.input.getActionMap().put("hide-command-completions", new AbstractAction() {
                @Override
                public void actionPerformed(ActionEvent e) {
                    hide();
                }
            });
            this.input.getDocument().addDocumentListener(new DocumentListener() {
                @Override
                public void insertUpdate(DocumentEvent e) {
                    refreshSuggestionsIfUserEdited();
                }

                @Override
                public void removeUpdate(DocumentEvent e) {
                    refreshSuggestionsIfUserEdited();
                }

                @Override
                public void changedUpdate(DocumentEvent e) {
                    refreshSuggestionsIfUserEdited();
                }
            });
            this.input.addCaretListener(e -> refreshSuggestionsIfUserEdited());
        }

        private void completeNext() {
            var completion = this.suggester.complete(this.input.getText(), this.input.getCaretPosition());
            var candidates = completion.candidates();
            if (candidates.isEmpty()) {
                hide();
                return;
            }

            var current = this.input.getText().substring(completion.replaceStart(), completion.replaceEnd());
            var candidate = this.chooseCandidate(candidates, current);
            this.applyCandidate(completion, candidate, this.shouldAppendSpace(candidates, candidate));
            this.refreshSuggestions();
        }

        private String chooseCandidate(List<String> candidates, String current) {
            if (candidates.size() == 1) {
                return candidates.getFirst();
            }

            var currentIndex = this.indexOfIgnoreCase(candidates, current);
            if (currentIndex == -1) {
                return candidates.getFirst();
            }

            return candidates.get((currentIndex + 1) % candidates.size());
        }

        private void applyCandidate(AllayCommandSuggester.Completion completion, String candidate, boolean appendSpace) {
            var text = this.input.getText();
            var replacement = appendSpace ? candidate + " " : candidate;
            this.completing = true;
            try {
                this.input.setText(text.substring(0, completion.replaceStart()) + replacement + text.substring(completion.replaceEnd()));
                this.input.setCaretPosition(completion.replaceStart() + replacement.length());
            } finally {
                this.completing = false;
            }
        }

        private boolean shouldAppendSpace(List<String> candidates, String candidate) {
            return candidates.stream()
                    .filter(value -> !value.equalsIgnoreCase(candidate))
                    .noneMatch(value -> startsWithIgnoreCase(value, candidate));
        }

        private int indexOfIgnoreCase(List<String> values, String value) {
            for (int i = 0; i < values.size(); i++) {
                if (values.get(i).equalsIgnoreCase(value)) {
                    return i;
                }
            }

            return -1;
        }

        private void refreshSuggestionsIfUserEdited() {
            if (!this.completing) {
                SwingUtilities.invokeLater(this::refreshSuggestions);
            }
        }

        private void refreshSuggestions() {
            if (!this.input.isShowing() || this.input.getText().isBlank()) {
                hide();
                return;
            }

            var completion = this.suggester.complete(this.input.getText(), this.input.getCaretPosition());
            var candidates = completion.candidates();
            if (candidates.isEmpty()) {
                this.hide();
                return;
            }

            this.showSuggestions(candidates, candidates.getFirst());
        }

        private void showSuggestions(List<String> candidates, String selected) {
            this.suggestionModel.clear();
            candidates.forEach(this.suggestionModel::addElement);
            this.suggestionList.setSelectedValue(selected, true);
            this.suggestionList.setPrototypeCellValue(candidates.stream().max(Comparator.comparingInt(String::length)).orElse(""));
            this.popup.setPopupSize(Math.max(this.input.getWidth(), 220), Math.min(180, 24 * Math.max(1, candidates.size()) + 6));
            if (!this.popup.isVisible()) {
                this.popup.show(this.input, 0, this.input.getHeight());
            }
        }

        private void hide() {
            this.popup.setVisible(false);
        }

        private boolean startsWithIgnoreCase(String value, String prefix) {
            if (prefix == null || prefix.isEmpty()) {
                return true;
            }

            return value.regionMatches(true, 0, prefix, 0, prefix.length());
        }
    }

    private static final class DashboardLogAppender extends AbstractAppender {
        private final Dashboard dashboard;

        private DashboardLogAppender(String name, Layout<? extends Serializable> layout, Dashboard dashboard) {
            super(name, null, layout, true, Property.EMPTY_ARRAY);
            this.dashboard = dashboard;
        }

        @Override
        public void append(LogEvent event) {
            dashboard.appendTextToConsole(getLayout().toSerializable(event).toString());
        }
    }

    private class CommandListener implements ActionListener {
        @Override
        public void actionPerformed(ActionEvent e) {
            if (commandAutocomplete != null) {
                commandAutocomplete.hide();
            }
            var cmd = cmdInput.getText().trim();
            cmdInput.setText(""); // clear the input
            if (cmd.isEmpty()) return;
            if (cmd.startsWith("/")) {
                cmd = cmd.substring(1);
            }
            if (cmd.isEmpty()) return;

            appendTextToConsole(cmd + "\n"); // show what was run in the console
            var server = Server.getInstance();
            var command = cmd;
            server.getScheduler().runLater(server, () -> Registries.COMMANDS.execute(Server.getInstance(), command));
        }
    }
}
