public interface MethodInterface {
    /**
     * Creates and load the different configuration files.
     */
    void loadFiles();

    /**
     * Request JSON from the <code>url</code> and extract the <code>key</code>
     *
     * @param url the url
     * @param key the key
     * @return the value corresponding to the key
     */
    String getFromUrlJson(String url, String key);

    /**
     * Get the advanced ban version.
     *
     * @return the version
     */
    String getVersion();

    /**
     * Get key from a config file at a <code>path</code>.
     *
     * @param file the file
     * @param path the path
     * @return the string [ ]
     */
    String[] getKeys(Object file, String path);

    /**
     * Get the config.yml file
     *
     * @return the config
     */
    Object getConfig();

    /**
     * Get the messages.yml file
     *
     * @return the messages
     */
    Object getMessages();

    /**
     * Get the layouts.yml file
     *
     * @return the layouts
     */
    Object getLayouts();

    /**
     * Set up metrics.
     */
    void setupMetrics();

    boolean isBungee();

    String clearFormatting(String text);

    /**
     * Get the plugin instance.
     *
     * @return the plugin
     */
    Object getPlugin();

    /**
     * Get the data folder for string punishments.
     *
     * @return the data folder
     */
    File getDataFolder();

    /**
     * Register a command name to be handled by advancedban.
     *
     * @param cmd the cmd
     * @param tabCompleter behaviour when tab completion is triggered
     */
    void setCommandExecutor(String cmd, TabCompleter tabCompleter);

    /**
     * Send a message to a specific player.
     *
     * @param player the player
     * @param msg    the msg
     */
    void sendMessage(Object player, String msg);

    /**
     * Get a players name.
     *
     * @param player the player
     * @return the name
     */
    String getName(Object player);

    /**
     * Get a players name based on the intern uuid fetcher.
     *
     * @param uuid the uuid
     * @return the name
     */
    String getName(String uuid);

    /**
     * Get a players ip.
     *
     * @param player the player
     * @return the ip
     */
    String getIP(Object player);

    /**
     * Get a players uuid based on the intern uuid fetcher.
     *
     * @param player the player
     * @return the intern uuid
     */
    String getInternUUID(Object player);

    /**
     * Get a players uuid based on the intern uuid fetcher.
     *
     * @param player the player
     * @return the intern uuid
     */
    String getInternUUID(String player);

    /**
     * Check if player has the given permission.
     *
     * @param player the player
     * @param perms  the perms
     * @return the boolean
     */
    boolean hasPerms(Object player, String perms);

    /**
     * Get a Permissionable player that can be checked for offline permissions.
     *
     * @param name the name of the user
     * @return the offline permission user
     */
    Permissionable getOfflinePermissionPlayer(String name);

    /**
     * Check whether player is online.
     *
     * @param name the name
     * @return the boolean
     */
    boolean isOnline(String name);

    /**
     * Get online player by name.
     *
     * @param name the name
     * @return the player
     */
    Object getPlayer(String name);

    /**
     * Kick a player.
     *
     * @param player the player
     * @param reason the reason
     */
    void kickPlayer(String player, String reason);

    /**
     * Get online players.
     *
     * @return the object [ ]
     */
    Object[] getOnlinePlayers();

    /**
     * Schedule async repeating task.
     *
     * @param rn the rn
     * @param l1 the l 1
     * @param l2 the l 2
     */
    void scheduleAsyncRep(Runnable rn, long l1, long l2);

    /**
     * Schedule async task.
     *
     * @param rn the rn
     * @param l1 the l 1
     */
    void scheduleAsync(Runnable rn, long l1);

    /**
     * Run async task.
     *
     * @param rn the rn
     */
    void runAsync(Runnable rn);

    /**
     * Run sync task.
     *
     * @param rn the rn
     */
    void runSync(Runnable rn);

    /**
     * (see implementation)
     *
     * @param cmd the cmd
     */
    void executeCommand(String cmd);

    /**
     * (see implementation)
     *
     * @param player the player
     * @return the boolean
     */
    boolean callChat(Object player);

    /**
     * (see implementation)
     *
     * @param player the player
     * @param cmd    the cmd
     * @return the boolean
     */
    boolean callCMD(Object player, String cmd);

    /**
     * Get MySQL.yml file.
     *
     * @return the my sql file
     */
    Object getMySQLFile();

    /**
     * Parse json string and retrieve value at given key.
     *
     * @param json the json
     * @param key  the key
     * @return the string
     */
    String parseJSON(InputStreamReader json, String key);

    /**
     * Parse json string and retrieve value at given key.
     *
     * @param json the json
     * @param key  the key
     * @return the string
     */
    String parseJSON(String json, String key);

    /**
     * Get boolean at the given path from the given file.
     *
     * @param file the file
     * @param path the path
     * @return the boolean
     */
    Boolean getBoolean(Object file, String path);

    /**
     * Get string at the given path from the given file.
     *
     * @param file the file
     * @param path the path
     * @return the string
     */
    String getString(Object file, String path);

    /**
     * Get long at the given path from the given file.
     *
     * @param file the file
     * @param path the path
     * @return the long
     */
    Long getLong(Object file, String path);

    /**
     * Get integer at the given path from the given file.
     *
     * @param file the file
     * @param path the path
     * @return the integer
     */
    Integer getInteger(Object file, String path);

    /**
     * Get string list at the given path from the given file.
     *
     * @param file the file
     * @param path the path
     * @return the string list
     */
    List<String> getStringList(Object file, String path);

    /**
     * Get boolean at the given path from the given file or default if not present.
     *
     * @param file the file
     * @param path the path
     * @param def  the def
     * @return the boolean
     */
    boolean getBoolean(Object file, String path, boolean def);

    /**
     * Get string at the given path from the given file or default if not present.
     *
     * @param file the file
     * @param path the path
     * @param def  the def
     * @return the string
     */
    String getString(Object file, String path, String def);

    /**
     * Get long at the given path from the given file or default if not present.
     *
     * @param file the file
     * @param path the path
     * @param def  the def
     * @return the long
     */
    long getLong(Object file, String path, long def);

    /**
     * Get integer at the given path from the given file or default if not present.
     *
     * @param file the file
     * @param path the path
     * @param def  the def
     * @return the integer
     */
    int getInteger(Object file, String path, int def);

    /**
     * Check whether file contains given path.
     *
     * @param file the file
     * @param path the path
     * @return the boolean
     */
    boolean contains(Object file, String path);

    /**
     * Get file name.
     *
     * @param file the file
     * @return the file name
     */
    String getFileName(Object file);

    /**
     * Call punishment event.
     *
     * @param punishment the punishment
     */
    void callPunishmentEvent(Punishment punishment);

    /**
     * Call revoke punishment event.
     *
     * @param punishment the punishment
     * @param massClear  the mass clear
     */
    void callRevokePunishmentEvent(Punishment punishment, boolean massClear);

    /**
     * Check whether server is in online mode.
     * Should indicate whether the intern uuid fetcher is going to work.
     *
     * @return the boolean
     */
    boolean isOnlineMode();

    /**
     * Broadcast a message to every user with the given permission.
     *
     * @param perm         the perm
     * @param notification the notification
     */
    void notify(String perm, List<String> notification);

    /**
     * Log a message.
     *
     * @param msg the msg
     */
    void log(String msg);

    /**
     * Whether this instance is used for unit testing.
     *
     * @return the boolean
     */
    boolean isUnitTesting();
} public class Universal {

    private static Universal instance = null;

    public static void setRedis(boolean redis) {
        Universal.redis = redis;
    }

    private final Map<String, String> ips = new HashMap<>();
    private MethodInterface mi;
    private LogManager logManager;

    private static boolean redis = false;


    private final Gson gson = new Gson();





    /**
     * Get universal.
     *
     * @return the universal instance
     */
    public static Universal get() {
        return instance == null ? instance = new Universal() : instance;
    }

    /**
     * Initially sets up the plugin.
     *
     * @param mi the mi
     */
    public void setup(MethodInterface mi) {
        this.mi = mi;
        mi.loadFiles();
        logManager = new LogManager();
        UpdateManager.get().setup();
        UUIDManager.get().setup();

        try {
            DatabaseManager.get().setup(mi.getBoolean(mi.getConfig(), "UseMySQL", false));
        } catch (Exception ex) {
            log("Failed enabling database-manager...");
            debugException(ex);
        }

        mi.setupMetrics();
        PunishmentManager.get().setup();

        for (Command command : Command.values()) {
            for (String commandName : command.getNames()) {
                mi.setCommandExecutor(commandName, command.getTabCompleter());
            }
        }

        String upt = "You have the newest version";
        String response = getFromURL("https://api.spigotmc.org/legacy/update.php?resource=8695");
        if (response == null) {
            upt = "Failed to check for updates :(";
        } else if ((!mi.getVersion().startsWith(response))) {
            upt = "There is a new version available! [" + response + "]";
        }

        if (mi.getBoolean(mi.getConfig(), "DetailedEnableMessage", true)) {
            mi.log("\n \n&8[]=====[&7Enabling AdvancedBan&8]=====[]&r"
                    + "\n&8| &cInformation:&r"
                    + "\n&8|   &cName: &7AdvancedBan&r"
                    + "\n&8|   &cDeveloper: &7Leoko&r"
                    + "\n&8|   &cVersion: &7" + mi.getVersion() + "&r"
                    + "\n&8|   &cStorage: &7" + (DatabaseManager.get().isUseMySQL() ? "MySQL (external)" : "HSQLDB (local)") + "&r"
                    + "\n&8| &cSupport:&r"
                    + "\n&8|   &cGithub: &7https://github.com/DevLeoko/AdvancedBan/issues &r"
                    + "\n&8|   &cDiscord: &7https://discord.gg/ycDG6rS &r"
                    + "\n&8| &cTwitter: &7@LeokoGar&r"
                    + "\n&8| &cUpdate:&r"
                    + "\n&8|   &7" + upt  + "&r"
                    + "\n&8[]================================[]&r\n ");
        } else {
            mi.log("&cEnabling AdvancedBan on Version &7&r" + mi.getVersion());
            mi.log("&cCoded by &7Leoko &8| &7Twitter: @LeokoGar&r");
        }
    }

    /**
     * Shutdown.
     */
    public void shutdown() {
        DatabaseManager.get().shutdown();

        if (mi.getBoolean(mi.getConfig(), "DetailedDisableMessage", true)) {
            mi.log("\n \n&8[]=====[&7Disabling AdvancedBan&8]=====[]"
                    + "\n&8| &cInformation:"
                    + "\n&8|   &cName: &7AdvancedBan"
                    + "\n&8|   &cDeveloper: &7Leoko"
                    + "\n&8|   &cVersion: &7" + getMethods().getVersion()
                    + "\n&8|   &cStorage: &7" + (DatabaseManager.get().isUseMySQL() ? "MySQL (external)" : "HSQLDB (local)")
                    + "\n&8| &cSupport:"
                    + "\n&8|   &cGithub: &7https://github.com/DevLeoko/AdvancedBan/issues"
                    + "\n&8|   &cDiscord: &7https://discord.gg/ycDG6rS"
                    + "\n&8| &cTwitter: &7@LeokoGar"
                    + "\n&8[]================================[]&r\n ");
        } else {
            mi.log("&cDisabling AdvancedBan on Version &7" + getMethods().getVersion());
            mi.log("&cCoded by Leoko &8| &7Twitter: @LeokoGar");
        }
    }

    /**
     * Gets methods.
     *
     * @return the methods
     */
    public MethodInterface getMethods() {
        return mi;
    }

    /**
     * Is bungee boolean.
     *
     * @return the boolean
     */
    public boolean isBungee() {
        return mi.isBungee();
    }

    public Map<String, String> getIps() {
        return ips;
    }

    public static boolean isRedis() {
        return redis;
    }

    public Gson getGson() {
        return gson;
    }

    /**
     * Gets from url.
     *
     * @param surl the surl
     * @return the from url
     */
    public String getFromURL(String surl) {
        String response = null;
        try {
            URL url = new URL(surl);
            Scanner s = new Scanner(url.openStream());
            if (s.hasNext()) {
                response = s.next();
                s.close();
            }
        } catch (IOException exc) {
            debug("!! Failed to connect to URL: " + surl);
        }
        return response;
    }

    /**
     * Is mute command boolean.
     *
     * @param cmd the cmd
     * @return the boolean
     */
    public boolean isMuteCommand(String cmd) {
        return isMuteCommand(cmd, getMethods().getStringList(getMethods().getConfig(), "MuteCommands"));
    }

    /**
     * Visible for testing. Do not use this. Please use {@link #isMuteCommand(String)}.
     * 
     * @param cmd          the command
     * @param muteCommands the mute commands from the config
     * @return true if the command matched any of the mute commands.
     */
    boolean isMuteCommand(String cmd, List<String> muteCommands) {
        String[] words = cmd.split(" ");
        // Handle commands with colons
        if (words[0].indexOf(':') != -1) {
            words[0] = words[0].split(":", 2)[1];
        }
        for (String muteCommand : muteCommands) {
            if (muteCommandMatches(words, muteCommand)) {
                return true;
            }
        }
        return false;
    }

    /**
     * Visible for testing. Do not use this.
     * 
     * @param commandWords the command run by a player, separated into its words
     * @param muteCommand a mute command from the config
     * @return true if they match, false otherwise
     */
    boolean muteCommandMatches(String[] commandWords, String muteCommand) {
        // Basic equality check
        if (commandWords[0].equalsIgnoreCase(muteCommand)) {
            return true;
        }
        // Advanced equality check
        // Essentially a case-insensitive "startsWith" for arrays
        if (muteCommand.indexOf(' ') != -1) {
            String[] muteCommandWords = muteCommand.split(" ");
            if (muteCommandWords.length > commandWords.length) {
                return false;
            }
            for (int n = 0; n < muteCommandWords.length; n++) {
                if (!muteCommandWords[n].equalsIgnoreCase(commandWords[n])) {
                    return false;
                }
            }
            return true;
        }
        return false;
    }

    /**
     * Is exempt player boolean.
     *
     * @param name the name
     * @return the boolean
     */
    public boolean isExemptPlayer(String name) {
        List<String> exempt = getMethods().getStringList(getMethods().getConfig(), "ExemptPlayers");
        if (exempt != null) {
            for (String str : exempt) {
                if (name.equalsIgnoreCase(str)) {
                    return true;
                }
            }
        }
        return false;
    }

    /**
     * Broadcast leoko boolean.
     *
     * @return the boolean
     */
    public boolean broadcastLeoko() {
        File readme = new File(getMethods().getDataFolder(), "readme.txt");
        if (!readme.exists()) {
            return true;
        }
        try {
            if (Files.readAllLines(Paths.get(readme.getPath()), Charset.defaultCharset()).get(0).equalsIgnoreCase("I don't want that there will be any message when the dev of this plugin joins the server! I want this even though the plugin is 100% free and the join-message is the only reward for the Dev :(")) {
                return false;
            }
        } catch (IOException ignore) {
        }
        return true;
    }

    /**
     * Call connection string.
     *
     * @param name the name
     * @param ip   the ip
     * @return the string
     */
    public String callConnection(String name, String ip) {
        name = name.toLowerCase();
        String uuid = UUIDManager.get().getUUID(name);
        if (uuid == null) return "[AdvancedBan] Failed to fetch your UUID";

        if (ip != null) {
            getIps().remove(name);
            getIps().put(name, ip);
        }

        InterimData interimData = PunishmentManager.get().load(name, uuid, ip);

        if (interimData == null) {
            if (getMethods().getBoolean(mi.getConfig(), "LockdownOnError", true)) {
                return "[AdvancedBan] Failed to load player data!";
            } else {
                return null;
            }
        }

        Punishment pt = interimData.getBan();

        if (pt == null) {
            interimData.accept();
            return null;
        }

        return pt.getLayoutBSN();
    }

    /**
     * Has perms boolean.
     *
     * @param player the player
     * @param perms  the perms
     * @return the boolean
     */
    public boolean hasPerms(Object player, String perms) {
        if (mi.hasPerms(player, perms)) {
            return true;
        }

        if (mi.getBoolean(mi.getConfig(), "EnableAllPermissionNodes", false)) {
            while (perms.contains(".")) {
                perms = perms.substring(0, perms.lastIndexOf('.'));
                if (mi.hasPerms(player, perms + ".all")) {
                    return true;
                }
            }
        }
        return false;
    }

    /**
     * Log.
     *
     * @param msg the msg
     */
    public void log(String msg) {
        mi.log("§8[§cAdvancedBan§8] §7" + msg);
        debugToFile(msg);
    }

    /**
     * Debug.
     *
     * @param msg the msg
     */
    public void debug(Object msg) {
        if (mi.getBoolean(mi.getConfig(), "Debug", false)) {
            mi.log("§8[§cAdvancedBan§8] §cDebug: §7" + msg.toString());
        }
        debugToFile(msg);
    }

    public void debugException(Exception exc) {
        StringWriter sw = new StringWriter();
        PrintWriter pw = new PrintWriter(sw);
        exc.printStackTrace(pw);
        debug(sw.toString());
    }

    /**
     * Debug.
     *
     * @param ex the ex
     */
    public void debugSqlException(SQLException ex) {
        if (mi.getBoolean(mi.getConfig(), "Debug", false)) {
            debug("§7An error has occurred with the database, the error code is: '" + ex.getErrorCode() + "'");
            debug("§7The state of the sql is: " + ex.getSQLState());
            debug("§7Error message: " + ex.getMessage());
        }
        debugException(ex);
    }

    private void debugToFile(Object msg) {
        File debugFile = new File(mi.getDataFolder(), "logs/latest.log");
        if (!debugFile.exists()) {
            try {
                debugFile.createNewFile();
            } catch (IOException ex) {
                System.out.print("An error has occurred creating the 'latest.log' file again, check your server.");
                System.out.print("Error message" + ex.getMessage());
            }
        } else {
            logManager.checkLastLog(false);
        }
        try {
            FileUtils.writeStringToFile(debugFile, "[" + new SimpleDateFormat("HH:mm:ss").format(System.currentTimeMillis()) + "] " + mi.clearFormatting(msg.toString()) + "\n", "UTF8", true);
        } catch (IOException ex) {
            System.out.print("An error has occurred writing to 'latest.log' file.");
            System.out.print(ex.getMessage());
        }
    }
} public class DatabaseManager {

    private HikariDataSource dataSource;
    private boolean useMySQL;

    private RowSetFactory factory;
    
    private static DatabaseManager instance = null;

    /**
     * Get the instance of the command manager
     *
     * @return the database manager instance
     */
    public static synchronized DatabaseManager get() {
        return instance == null ? instance = new DatabaseManager() : instance;
    }

    /**
     * Initially connects to the database and sets up the required tables of they don't already exist.
     *
     * @param useMySQLServer whether to preferably use MySQL (uses HSQLDB as fallback)
     */
    public void setup(boolean useMySQLServer) {
        useMySQL = useMySQLServer;

        try {
            dataSource = new DynamicDataSource(useMySQL).generateDataSource();
        } catch (ClassNotFoundException ex) {
            Universal.get().log("§cERROR: Failed to configure data source!");
            Universal.get().debug(ex.getMessage());
            return;
        }

        executeStatement(SQLQuery.CREATE_TABLE_PUNISHMENT);
        executeStatement(SQLQuery.CREATE_TABLE_PUNISHMENT_HISTORY);
    }

    /**
     * Shuts down the HSQLDB if used.
     */
    public void shutdown() {
        if (!useMySQL) {
            try(Connection connection = dataSource.getConnection(); final PreparedStatement statement = connection.prepareStatement("SHUTDOWN")){
                statement.execute();
            }catch (SQLException | NullPointerException exc){
                Universal.get().log("An unexpected error has occurred turning off the database");
                Universal.get().debugException(exc);
            }
        }

        dataSource.close();
    }
    
    private CachedRowSet createCachedRowSet() throws SQLException {
    	if (factory == null) {
    		factory = RowSetProvider.newFactory();
    	}
    	return factory.createCachedRowSet();
    }

    /**
     * Execute a sql statement without any results.
     *
     * @param sql        the sql statement
     * @param parameters the parameters
     */
    public void executeStatement(SQLQuery sql, Object... parameters) {
        executeStatement(sql, false, parameters);
    }

    /**
     * Execute a sql statement.
     *
     * @param sql        the sql statement
     * @param parameters the parameters
     * @return the result set
     */
    public ResultSet executeResultStatement(SQLQuery sql, Object... parameters) {
        return executeStatement(sql, true, parameters);
    }

    private ResultSet executeStatement(SQLQuery sql, boolean result, Object... parameters) {
        return executeStatement(sql.toString(), result, parameters);
    }

    private synchronized ResultSet executeStatement(String sql, boolean result, Object... parameters) {
    	try (Connection connection = dataSource.getConnection(); PreparedStatement statement = connection.prepareStatement(sql)) {

    		for (int i = 0; i < parameters.length; i++) {
    			statement.setObject(i + 1, parameters[i]);
    		}

    		if (result) {
    			CachedRowSet results = createCachedRowSet();
    			results.populate(statement.executeQuery());
    			return results;
    		}
   			statement.execute();
    	} catch (SQLException ex) {
    		Universal.get().log(
   					"An unexpected error has occurred executing an Statement in the database\n"
   							+ "Please check the plugins/AdvancedBan/logs/latest.log file and report this "
    						+ "error in: https://github.com/DevLeoko/AdvancedBan/issues"
    				);
    		Universal.get().debug("Query: \n" + sql);
    		Universal.get().debugSqlException(ex);
       	} catch (NullPointerException ex) {
            Universal.get().log(
                    "An unexpected error has occurred connecting to the database\n"
                            + "Check if your MySQL data is correct and if your MySQL-Server is online\n"
                            + "Please check the plugins/AdvancedBan/logs/latest.log file and report this "
                            + "error in: https://github.com/DevLeoko/AdvancedBan/issues"
            );
            Universal.get().debugException(ex);
        }
        return null;
    }

    /**
     * Check whether there is a valid connection to the database.
     *
     * @return whether there is a valid connection
     */
    public boolean isConnectionValid() {
        return dataSource.isRunning();
    }

    /**
     * Check whether MySQL is actually used.
     *
     * @return whether MySQL is used
     */
    public boolean isUseMySQL() {
        return useMySQL;
    }
} public class MessageManager {

    private static MethodInterface mi() {
    	return Universal.get().getMethods();
    }

    /**
     * Get the message from the given path.<br>
     * The parameters work as described in {@link #sendMessage(Object, String, boolean, String...)}.
     *
     * @param path       the path
     * @param parameters the parameters
     * @return the message
     */
    public static String getMessage(String path, String... parameters) {
    	MethodInterface mi = mi();
        String str = mi.getString(mi.getMessages(), path);
        if (str == null) {
            str = "Failed! See console for details!";
            System.out.println("!! Message-Error!\n"
                    + "In order to solve the problem please:"
                    + "\n  - Check the Message.yml-File for any missing or double \" or '"
                    + "\n  - Visit yamllint.com to  validate your Message.yml"
                    + "\n  - Delete the message file and restart the server");
        } else {
            str = replace(str, parameters).replace('&', '§');
        }
        return str;
    }


    /**
     * Get the message from the given path.<br>
     * The parameters work as described in {@link #sendMessage(Object, String, boolean, String...)}.
     *
     * @param path       the path
     * @param prefix     whether to prepend a prefix (can be overridden by the DisablePrefix option)
     * @param parameters the parameters
     * @return the message
     */
    public static String getMessage(String path, boolean prefix, String... parameters) {
    	MethodInterface mi = mi();
        String prefixStr = "";
        if(prefix && !mi.getBoolean(mi.getConfig(), "Disable Prefix", false))
            prefixStr = getMessage("General.Prefix")+" ";

        return prefixStr+getMessage(path, parameters);
    }

    /**
     * Get the layout (basically just a string list) from the given path in the given file.<br>
     * The parameters work as described in {@link #sendMessage(Object, String, boolean, String...)}.
     *
     * @param file       the file (see {@link MethodInterface#getConfig()}, {@link MethodInterface#getMessages()},
     *                   {@link MethodInterface#getLayouts()})
     * @param path       the path
     * @param parameters the parameters
     * @return the layout
     */
    public static List<String> getLayout(Object file, String path, String... parameters) {
    	MethodInterface mi = mi();
        if (mi.contains(file, path)) {
            List<String> list = new ArrayList<>();
            for (String str : mi.getStringList(file, path)) {
                list.add(replace(str, parameters).replace('&', '§'));
            }
            return list;
        }
        String fileName = mi.getFileName(file);
		System.out.println("!! Message-Error in " + fileName + "!\n"
		        + "In order to solve the problem please:"
		        + "\n  - Check the " + fileName + "-File for any missing or double \" or '"
		        + "\n  - Visit yamllint.com to  validate your " + fileName
		        + "\n  - Delete the message file and restart the server");
		return Collections.singletonList("Failed! See console for details!");
    }

    /**
     * Send message from the given path directly to the given receiver.<br><br>
     * <b>How the <code>parameters</code> work:</b>
     * The amount of parameters given has to be an even number as the parameters are interpreted in pairs.<br>
     * The first parameter is the String to search for and the second one is the one it will be replaced with.<br>
     * Same goes for the third and fourth and for the fifth and sixth and so on.<br><br>
     * <b>e.g.:</b> <code>getMessage("some.path", "NAME", "Leoko", "ID", "#342")</code> will get the message located at
     * "some.path" and replace each <i>%NAME%</i> with <i>Leoko</i> and each <i>%ID%</i> with <i>#342</i>.
     *
     * @param receiver   the receiver (Bukkit or Bungeecord player object)
     * @param path       the path
     * @param prefix     whether to use the global prefix
     * @param parameters the parameters
     */
    public static void sendMessage(Object receiver, String path, boolean prefix, String... parameters) {
    	MethodInterface mi = mi();
        final String message = getMessage(path, parameters);
        if(!message.isEmpty()) {
            final String prefixString = prefix && !mi.getBoolean(mi.getConfig(), "Disable Prefix", false) ? getMessage("General.Prefix") + " " : "";
            mi.sendMessage(receiver, prefixString + message);
        }
    }

    private static String replace(String str, String... parameters) {
        for (int i = 0; i < parameters.length - 1; i = i + 2) {
            str = str.replaceAll("%" + parameters[i] + "%", parameters[i + 1]);
        }
        return str;
    }
} public class PunishmentManager {

    private static PunishmentManager instance = null;
    private final Set<Punishment> punishments = Collections.synchronizedSet(new HashSet<>());
    private final Set<Punishment> history = Collections.synchronizedSet(new HashSet<>());
    private final Set<String> cached = Collections.synchronizedSet(new HashSet<>());
    
    private Universal universal() {
    	return Universal.get();
    }

    /**
     * Get the punishment manager.
     *
     * @return the punishment manager instance
     */
    public static synchronized PunishmentManager get() {
        return instance == null ? instance = new PunishmentManager() : instance;
    }

    /**
     * Initially clears out all expired punishments.
     */
    public void setup() {
        DatabaseManager.get().executeStatement(SQLQuery.DELETE_OLD_PUNISHMENTS, TimeManager.getTime());
        // Seems useless as the Interim Data which get's loaded just is ignored
//        for (Object player : mi.getOnlinePlayers()) {
//            String name = mi.getName(player).toLowerCase();
//            load(name, UUIDManager.get().getUUID(name), mi.getIP(player));
//        }
    }

    /**
     * Get a users punishments as {@link InterimData}. This method is meant to be called if the goal eventually is
     * to add the users punishments to the cache. If you are just interested in the specific punishments there are
     * more convenient methods as {@link #getBan(String)}, {@link #getMute(String)}, {@link #getWarns(String)}
     * or {@link #getPunishments(String, PunishmentType, boolean)}.
     *
     * @param name the users name
     * @param uuid the users uuid
     * @param ip   the users ip
     * @return the interim data
     */
    public InterimData load(String name, String uuid, String ip) {
        Set<Punishment> punishments = new HashSet<>();
        Set<Punishment> history = new HashSet<>();
        try (ResultSet resultsPunishments = DatabaseManager.get().executeResultStatement(SQLQuery.SELECT_USER_PUNISHMENTS_WITH_IP, uuid, ip); ResultSet resultsHistory = DatabaseManager.get().executeResultStatement(SQLQuery.SELECT_USER_PUNISHMENTS_HISTORY_WITH_IP, uuid, ip)) {
            if (resultsHistory == null || resultsPunishments == null)
                return null;

            while (resultsPunishments.next()) {
                punishments.add(getPunishmentFromResultSet(resultsPunishments));
            }
            while (resultsHistory.next()) {
                history.add(getPunishmentFromResultSet(resultsHistory));
            }

        } catch (SQLException ex) {
        	Universal universal = universal();
            universal.log("An error has occurred loading the punishments from the database.");
            universal.debugSqlException(ex);
            return null;
        }
        return new InterimData(uuid, name, ip, punishments, history);
    }

    /**
     * Discard a players punishments from the cache.
     *
     * @param name the name
     */
    public void discard(String name) {
        name = name.toLowerCase();
        String ip = Universal.get().getIps().get(name);
        String uuid = UUIDManager.get().getUUID(name);
        cached.remove(name);
        cached.remove(uuid);
        cached.remove(ip);

        Iterator<Punishment> iterator = punishments.iterator();
        while (iterator.hasNext()) {
            Punishment punishment = iterator.next();
            if (punishment.getUuid().equals(uuid) || punishment.getUuid().equals(ip)) {
                iterator.remove();
            }
        }

        iterator = history.iterator();
        while (iterator.hasNext()) {
            Punishment punishment = iterator.next();
            if (punishment.getUuid().equals(uuid) || punishment.getUuid().equals(ip)) {
                iterator.remove();
            }
        }
    }

    /**
     * Get all punishments which belong to the given uuid or ip.
     *
     * @param target  the uuid or ip to search for
     * @param put     the basic punishment type to search for ({@link PunishmentType#BAN} would also include Tempbans).
     *                Use <code>null</code> to search for all punishments.
     * @param current if only active punishments should be included.
     * @return the punishments
     */
    public List<Punishment> getPunishments(String target, PunishmentType put, boolean current) {
        List<Punishment> ptList = new ArrayList<>();

        if (isCached(target)) {
            for (Iterator<Punishment> iterator = (current ? punishments : history).iterator(); iterator.hasNext(); ) {
                Punishment pt = iterator.next();
                if ((put == null || put == pt.getType().getBasic()) && pt.getUuid().equals(target)) {
                    if (!current || !pt.isExpired()) {
                        ptList.add(pt);
                    } else {
                        pt.delete(null, false, false);
                        iterator.remove();
                    }
                }
            }
        } else {
            try (ResultSet rs = DatabaseManager.get().executeResultStatement(current ? SQLQuery.SELECT_USER_PUNISHMENTS : SQLQuery.SELECT_USER_PUNISHMENTS_HISTORY, target)) {
                while (rs.next()) {
                    Punishment punishment = getPunishmentFromResultSet(rs);
                    if ((put == null || put == punishment.getType().getBasic()) && (!current || !punishment.isExpired())) {
                        ptList.add(punishment);
                    }
                }
            } catch (SQLException ex) {
            	Universal universal = universal();
                universal.log("An error has occurred getting the punishments for " + target);
                universal.debugSqlException(ex);
            }
        }
        return ptList;
    }

    /**
     * Get parsed punishments from the database queried by the given {@link SQLQuery}.<br>
     * The parameters work as described in {@link MessageManager#sendMessage(Object, String, boolean, String...)}.
     *
     * @param sqlQuery   the sql query
     * @param parameters the parameters
     * @return the punishments
     */
    public List<Punishment> getPunishments(SQLQuery sqlQuery, Object... parameters) {
        List<Punishment> ptList = new ArrayList<>();

        ResultSet rs = DatabaseManager.get().executeResultStatement(sqlQuery, parameters);
        try {
            while (rs.next()) {
                Punishment punishment = getPunishmentFromResultSet(rs);
                ptList.add(punishment);
            }
            rs.close();
        } catch (SQLException ex) {
        	Universal universal = universal();
            universal.log("An error has occurred executing a query in the database.");
            universal.debug("Query: \n" + sqlQuery);
            universal.debugSqlException(ex);
        }
        return ptList;
    }

    /**
     * Get an active punishment by id.
     *
     * @param id the id
     * @return the punishment
     */
    public Punishment getPunishment(int id) {
        final Optional<Punishment> cachedPunishment = getLoadedPunishments(false).stream()
                .filter(punishment -> punishment.getId() == id).findAny();

        if (cachedPunishment.isPresent())
            return cachedPunishment.get();


        try (ResultSet rs = DatabaseManager.get().executeResultStatement(SQLQuery.SELECT_PUNISHMENT_BY_ID, id)) {
            if (rs.next()) {
                final Punishment punishment = getPunishmentFromResultSet(rs);
                if (!punishment.isExpired())
                    return punishment;
            }
        } catch (SQLException ex) {
        	Universal universal = universal();
            universal.log("An error has occurred getting a punishment by his id.");
            universal.debug("Punishment id: '" + id + "'");
            universal.debugSqlException(ex);
        }

        return null;
    }

    /**
     * Get an active warning by id.
     *
     * @param id the id
     * @return the warning
     */
    public Punishment getWarn(int id) {
        Punishment punishment = getPunishment(id);

        if (punishment == null)
            return null;

        return punishment.getType().getBasic() == PunishmentType.WARNING ? punishment : null;
    }

    /**
     * Get a players active warnings.
     *
     * @param uuid the players uuid
     * @return the warns
     */
    public List<Punishment> getWarns(String uuid) {
        return getPunishments(uuid, PunishmentType.WARNING, true);
    } /**
     * Get an active note by id.
    *
    * @param id the id
    * @return the note
    */
   public Punishment getNote(int id) {
       Punishment punishment = getPunishment(id);

       if (punishment == null)
           return null;

       return punishment.getType().getBasic() == PunishmentType.NOTE ? punishment : null;
   }

   /**
    * Get a players active note.
    *
    * @param uuid the players uuid
    * @return the note
    */
   public List<Punishment> getNotes(String uuid) {
       return getPunishments(uuid, PunishmentType.NOTE, true);
   }

    /**
     * Get a players active ban.
     *
     * @param uuid the players uuid (can also be an IP)
     * @return the ban or <code>null</code> if not banned
     */
    public Punishment getBan(String uuid) {
        List<Punishment> punishments = getPunishments(uuid, PunishmentType.BAN, true);
        return punishments.isEmpty() ? null : punishments.get(0);
    }

    /**
     * Get a players active mute.
     *
     * @param uuid the players uuid
     * @return the mute or <code>null</code> if not muted
     */
    public Punishment getMute(String uuid) {
        List<Punishment> punishments = getPunishments(uuid, PunishmentType.MUTE, true);
        return punishments.isEmpty() ? null : punishments.get(0);
    }

    /**
     * Check whether a player is banned.
     *
     * @param uuid the players uuid (can also be an IP)
     * @return whether the player is banned
     */
    public boolean isBanned(String uuid) {
        return getBan(uuid) != null;
    }

    /**
     * Check whether a player is muted.
     *
     * @param uuid the players uuid
     * @return whether the player is muted
     */
    public boolean isMuted(String uuid) {
        return getMute(uuid) != null;
    }

    /**
     * Check whether the data for the given uuid, ip or username are currently cached.
     *
     * @param target the target (uuid, ip or username)
     * @return whether the targets data is cached
     */
    public boolean isCached(String target) {
        return cached.contains(target);
    }

    /**
     * Mark the InterimData as cached.
     * This method des not acually cache the data, see {@link InterimData#accept()} to do that.
     *
     * @param data the data
     */
    public void setCached(InterimData data) {
        cached.add(data.getName());
        cached.add(data.getIp());
        cached.add(data.getUuid());
    }

    /**
     * Get punishment-time calculation level.
     * This level is represented by the amount a user has been punished using the given time-layout.
     *
     * @param uuid   the players uuid
     * @param layout the time-layout name
     * @return the calculation level
     */
    public int getCalculationLevel(String uuid, String layout) {
        if (isCached(uuid)) {
            return (int) history.stream().filter(pt -> pt.getUuid().equals(uuid) && layout.equalsIgnoreCase(pt.getCalculation())).count();
        }

        int i = 0;
        try (ResultSet resultSet = DatabaseManager.get().executeResultStatement(SQLQuery.SELECT_USER_PUNISHMENTS_HISTORY_BY_CALCULATION, uuid, layout)) {

            while (resultSet.next()) {
                i++;
            }

        } catch (SQLException ex) {
        	Universal universal = universal();
            universal.log("An error has occurred getting the level for the layout '" + layout + "' for '" + uuid + "'");
            universal.debugSqlException(ex);
        }
        return i;
    }

    /**
     * Get how many warnings a player has.
     *
     * @param uuid the players uuid
     * @return the current warning count
     */
    public int getCurrentWarns(String uuid) {
        return getWarns(uuid).size();
    }
    /**
     * Get how many notes a player has.
     *
     * @param uuid the players uuid
     * @return the current note count
     */
    public int getCurrentNotes(String uuid) {
        return getNotes(uuid).size();
    }

    /**
     * Get all cached punishments.
     *
     * @param checkExpired whether to look for and remove expired punishments
     * @return the cached punishments
     */
    public Set<Punishment> getLoadedPunishments(boolean checkExpired) {
        if (checkExpired) {
            List<Punishment> toDelete = new ArrayList<>();
            for (Punishment pu : punishments) {
                if (pu.isExpired()) {
                    toDelete.add(pu);
                }
            }
            for (Punishment pu : toDelete) {
                pu.delete();
            }
        }
        return punishments;
    }

    /**
     * Get a Punishment from a {@link ResultSet}
     *
     * @param rs the result set
     * @return the punishment from the result set
     * @throws SQLException the sql exception
     */
    public Punishment getPunishmentFromResultSet(ResultSet rs) throws SQLException {
        return new Punishment(
                rs.getString("name"),
                rs.getString("uuid"), rs.getString("reason"),
                rs.getString("operator"),
                PunishmentType.valueOf(rs.getString("punishmentType")),
                rs.getLong("start"),
                rs.getLong("end"),
                rs.getString("calculation"),
                rs.getInt("id"));
    }

    /**
     * Get all cached history punishments.
     *
     * @return the loaded history
     */
    public Set<Punishment> getLoadedHistory() {
        return history;
    }


//    public long getCalculation(String layout, String name, String uuid) {
//        long end = TimeManager.getTime();
//        MethodInterface mi = Universal.get().getMethods();
//
//        int i = getCalculationLevel(name, uuid);
//
//        List<String> timeLayout = mi.getStringList(mi.getLayouts(), "Time." + layout);
//        String time = timeLayout.get(timeLayout.size() <= i ? timeLayout.size() - 1 : i);
//        long toAdd = TimeManager.toMilliSec(time.toLowerCase());
//        end += toAdd;
//
//        return end;
//    }
} public class UUIDManager {
    private static UUIDManager instance = null;
    private FetcherMode mode;
    private final Map<String, String> activeUUIDs = new HashMap<>();
    
    private MethodInterface mi() {
    	return Universal.get().getMethods();
    }

    /**
     * Get the uuid manager.
     *
     * @return the uuid manager instance
     */
    public static synchronized UUIDManager get() {
        return instance == null ? instance = new UUIDManager() : instance;
    }

    /**
     * Initially setup the uuid manager by determening which {@link FetcherMode} should be used
     * based on the configured preference and the servers capabilities.
     */
    public void setup() {
    	MethodInterface mi = mi();
        if (mi.getBoolean(mi.getConfig(), "UUID-Fetcher.Dynamic", true)) {
            if (!mi.isOnlineMode()) {
                mode = FetcherMode.DISABLED;
            } else {
                if (Universal.get().isBungee()) {
                    mode = FetcherMode.MIXED;
                } else {
                    mode = FetcherMode.INTERN;
                }
            }
        } else {
            if (!mi.getBoolean(mi.getConfig(), "UUID-Fetcher.Enabled", true)) {
                mode = FetcherMode.DISABLED;
            } else if (mi.getBoolean(mi.getConfig(), "UUID-Fetcher.Intern", false)) {
                mode = FetcherMode.INTERN;
            } else {
                mode = FetcherMode.RESTFUL;
            }
        }
    }

    /**
     * Initially request the uuid bypassing the cache.<br>
     * If request succeeds the uuid will be automatically entered into the cache.
     *
     * @param name the name
     * @return the uuid
     */
    public String getInitialUUID(String name) {
    	MethodInterface mi = mi();
        name = name.toLowerCase();
        if (mode == FetcherMode.DISABLED)
            return name;

        if (mode == FetcherMode.INTERN || mode == FetcherMode.MIXED) {
            String internUUID = mi.getInternUUID(name);
            if (mode == FetcherMode.INTERN || internUUID != null)
                return internUUID;
        }

        String uuid = null;
        try {
            uuid = askAPI(mi.getString(mi.getConfig(), "UUID-Fetcher.REST-API.URL"), name, mi.getString(mi.getConfig(), "UUID-Fetcher.REST-API.Key"));
        } catch (IOException e) {
            System.out.println("Error -> " + e.getMessage());
            System.out.println("!! Failed fetching UUID of " + name);
            System.out.println("!! Could not connect to REST-API under " + mi.getString(mi.getConfig(), "UUID-Fetcher.REST-API.URL"));
        }

        if (uuid == null) {
            System.out.println("Trying to fetch UUID form BackUp-API...");
            try {
                uuid = askAPI(mi.getString(mi.getConfig(), "UUID-Fetcher.BackUp-API.URL"), name, mi.getString(mi.getConfig(), "UUID-Fetcher.BackUp-API.Key"));
            } catch (IOException e) {
                System.out.println("!! Failed fetching UUID of " + name);
                System.out.println("!! Could not connect to REST-API under " + mi.getString(mi.getConfig(), "UUID-Fetcher.BackUp-API.URL"));
            }
        }

        if (uuid == null) {
            System.out.println("!! !! Warning we have not been able to fetch the UUID of the Player " + name);
            System.out.println("!! Make sure that the name is spelled correctly and if it is change your UUID-Fetcher settings!");
        }

        return uuid;
    }

    /**
     * Adds uuid to the cache
     *
     * @param name the name
     * @param uuid the uuid
     */
    public void supplyInternUUID(String name, UUID uuid) {
        if (mode == FetcherMode.INTERN || mode == FetcherMode.MIXED) {
            activeUUIDs.put(name.toLowerCase(), uuid.toString().replace("-", ""));
        }
    }

    /**
     * Convert String to UUID even if dashes are missing
     *
     * @param uuid
     * @return
     */
    public UUID fromString(String uuid) {
        if (!uuid.contains("-") && uuid.length() == 32)
            uuid = uuid
                    .replaceFirst(
                            "(\\p{XDigit}{8})(\\p{XDigit}{4})(\\p{XDigit}{4})(\\p{XDigit}{4})(\\p{XDigit}+)", "$1-$2-$3-$4-$5");

        return uuid.length() == 36 && uuid.contains("-") ? UUID.fromString(uuid) : null;
    }

    /**
     * Get the uuid to a name.
     *
     * @param name the name
     * @return the uuid
     */
    public String getUUID(String name) {
        String inMemoryUuid = getInMemoryUUID(name);
        return (inMemoryUuid != null) ? inMemoryUuid : getInitialUUID(name);
    }

    /**
     * Gets a uuid from a name only if AdvancedBan
     * already has the uuid/name mapping in memory.
     *
     * @param name the player name
     * @return the nonhyphenated uuid or null if not found
     */
    public String getInMemoryUUID(String name) {
        return activeUUIDs.get(name.toLowerCase());
    }

    /**
     * Gets a name from a uuid only if AdvancedBan
     * already has the uuid/name mapping in memory.
     *
     * @param uuid the uuid without hyphens
     * @return the player name or null if not found
     */
    public String getInMemoryName(String uuid) {
        for (Entry<String, String> rs : activeUUIDs.entrySet()) {
            if (rs.getValue().equalsIgnoreCase(uuid)) {
                return rs.getKey();
            }
        }
        return null;
    }

    /**
     * Get name from an uuid.
     *
     * @param uuid         the uuid
     * @param forceInitial whether to bypass the cache
     * @return the name from uuid
     */
    public String getNameFromUUID(String uuid, boolean forceInitial) {
    	MethodInterface mi = mi();
        if (mode == FetcherMode.DISABLED)
            return uuid;

        if (mode == FetcherMode.INTERN || mode == FetcherMode.MIXED) {
            String internName = mi.getName(uuid);
            if (mode == FetcherMode.INTERN || internName != null)
                return internName;
        }

        if (!forceInitial) {
            String inMemoryName = getInMemoryName(uuid);
            if (inMemoryName != null) {
                return inMemoryName;
            }
        }

        try (Scanner scanner = new Scanner(new URL("https://api.mojang.com/user/profiles/" + uuid + "/names").openStream(), "UTF-8")) {
            String s = scanner.useDelimiter("\\A").next();
            s = s.substring(s.lastIndexOf('{'), s.lastIndexOf('}') + 1);
            return mi.parseJSON(s, "name");
        } catch (Exception exc) {
            return null;
        }
    }



    private String askAPI(String url, String name, String key) throws IOException {
    	MethodInterface mi = mi();
        name = name.toLowerCase();
        HttpURLConnection request = (HttpURLConnection) new URL(url.replaceAll("%NAME%", name).replaceAll("%TIMESTAMP%", new Date().getTime() + "")).openConnection();
        request.connect();

        String uuid = mi.parseJSON(new InputStreamReader(request.getInputStream()), key);

        if (uuid == null) {
            System.out.println("!! Failed fetching UUID of " + name);
            System.out.println("!! Could not find key '" + key + "' in the servers response");
            System.out.println("!! Response: " + request.getResponseMessage());
        } else {
            activeUUIDs.put(name, uuid);
        }
        return uuid;
    }

    /**
     * Get the {@link FetcherMode} which is used.
     *
     * @return the mode
     */
    public FetcherMode getMode() {
        return mode;
    }

    /**
     * The fetcher-mode describes how the {@link UUIDManager} resolves UUIDs.
     */
    public enum FetcherMode {
        /**
         * No UUID Fetcher is used. The Username will be treated as an UUID.<br>
         * <b>Recommended for:</b> Servers running in offline mode (cracked).
         */
        DISABLED,

        /**
         * Uses the integrated uuid fetcher from spigot/bungeecord to resolved UUIDs.<br>
         * <b>Recommended for:</b> None (should not be used as a default setting /
         * maybe useful to avoid exceeding API rate limits.)
         */
        INTERN,

        /**
         * Tries to resolve the UUID using the {@link #INTERN} fetcher and uses the
         * {@link #RESTFUL} fetcher as a fallback.<br>
         * <b>Recommended for:</b> Spigot &amp; Bungeecord Servers running in online mode.
         */
        MIXED,

        /**
         * Resolves the UUID using the REST-Services configured in the config.yml.
         * <b>Recommended for:</b> Servers in offline mode which still try to keep track of name changes.
         */
        RESTFUL
    }
} public class ListProcessor implements Consumer<Command.CommandInput> {
    private Function<String, List<Punishment>> listSupplier;
    private String config;
    private boolean history;
    private boolean hasTarget;

    public ListProcessor(Function<String, List<Punishment>> listSupplier, String config, boolean history, boolean hasTarget) {
        this.listSupplier = listSupplier;
        this.config = config;
        this.history = history;
        this.hasTarget = hasTarget;
    }

    @Override
    public void accept(Command.CommandInput input) {
        String target = null;
        String name = "invalid";
        if (hasTarget) {
            target = input.getPrimary();
            name = target;
            if (!target.matches("^(?:[0-9]{1,3}\\.){3}[0-9]{1,3}$")) {
                target = processName(input);
                if (target == null)
                    return;
            } else {
                input.next();
            }
        }

        MethodInterface mi = Universal.get().getMethods();
        final List<Punishment> punishments = listSupplier.apply(target);
        if (punishments.isEmpty()) {
            MessageManager.sendMessage(input.getSender(), config + ".NoEntries",
                    true, "NAME", name);
            return;
        }

        punishments
                .stream()
                .filter(punishment -> punishment.isExpired() && !history)
                .forEach(punishment -> {
                    punishment.delete();
                    punishments.remove(punishment);
                });

        int page = input.hasNext() ? Integer.parseInt(input.getPrimary()) : 1;
        if (punishments.size() / 5.0 + 1 <= page) {
            MessageManager.sendMessage(input.getSender(), config + ".OutOfIndex",
                    true, "PAGE", page + "");
            return;
        }

        String prefix = MessageManager.getMessage("General.Prefix");
        List<String> header = MessageManager.getLayout(mi.getMessages(), config + ".Header",
                "PREFIX", prefix, "NAME", name);

        header.forEach(line -> mi.sendMessage(input.getSender(), line));


        SimpleDateFormat format = new SimpleDateFormat(mi.getString(mi.getConfig(),
                "DateFormat", "dd.MM.yyyy-HH:mm"));

        for (int i = (page - 1) * 5; i < page * 5 && punishments.size() > i; i++) {
            Punishment punishment = punishments.get(i);
            String nameOrIp = punishment.getType().isIpOrientated() ? punishment.getName() + " / " +punishment.getUuid() : punishment.getName();
            List<String> entryLayout = MessageManager.getLayout(mi.getMessages(), config + ".Entry",
                    "PREFIX", prefix,
                    "NAME", nameOrIp,
                    "DURATION", punishment.getDuration(history),
                    "OPERATOR", punishment.getOperator(),
                    "REASON", punishment.getReason(),
                    "TYPE", punishment.getType().getName(),
                    "ID", punishment.getId() + "",
                    "DATE", format.format(new Date(punishment.getStart())));

            for (String line : entryLayout)
                mi.sendMessage(input.getSender(), line);
        }

        MessageManager.sendMessage(input.getSender(), config + ".Footer", false,
                "CURRENT_PAGE", page + "",
                "TOTAL_PAGES", (punishments.size() / 5 + (punishments.size() % 5 != 0 ? 1 : 0)) + "",
                "COUNT", punishments.size() + "");
        if (punishments.size() / 5.0 + 1 > page + 1) {
            MessageManager.sendMessage(input.getSender(), config + ".PageFooter", false,
                    "NEXT_PAGE", (page + 1) + "", "NAME", name);
        }
    }
} public class PunishmentProcessor implements Consumer<Command.CommandInput> {
    private PunishmentType type;

    public PunishmentProcessor(PunishmentType type) {
        this.type = type;
    }

    @Override
    public void accept(Command.CommandInput input) {
        boolean silent = processTag(input, "-s");
        String name = input.getPrimary();

        // extract target
        String target = type.isIpOrientated()
                ? processIP(input)
                : processName(input);
        if (target == null)
            return;

        // is exempted
        if (processExempt(name, target, input.getSender(), type))
            return;

        // calculate duration if necessary
        Long end = -1L;
        String timeTemplate = "";
        if (type.isTemp()) {
            TimeCalculation calculation = processTime(input, target, type);
            if (calculation == null)
                return;

            end = calculation.time;

            if (calculation.template != null)
                timeTemplate = calculation.template;
        }


        // build reason
        String reason = processReason(input);
        if (reason == null)
            return;
        else if (reason.isEmpty())
            reason = null;

        // check if punishment of this type is already active
        if (alreadyPunished(target, type)) {
            MessageManager.sendMessage(input.getSender(), type.getBasic().getName() + ".AlreadyDone",
                    true, "NAME", name);
            return;
        }

        MethodInterface mi = Universal.get().getMethods();
        String operator = mi.getName(input.getSender());
        Punishment.create(name, target, reason, operator, type, end, timeTemplate, silent);

        MessageManager.sendMessage(input.getSender(), type.getBasic().getName() + ".Done",
                true, "NAME", name);
    }

    // Removes time argument and returns timestamp (null if failed)
    private static TimeCalculation processTime(Command.CommandInput input, String uuid, PunishmentType type) {
        String time = input.getPrimary();
        input.next();
        MethodInterface mi = Universal.get().getMethods();
        if (time.matches("#.+")) {
            String layout = time.substring(1);
            if (!mi.contains(mi.getLayouts(), "Time." + layout)) {
                MessageManager.sendMessage(input.getSender(), "General.LayoutNotFound", true, "NAME", layout);
                return null;
            }
            int i = PunishmentManager.get().getCalculationLevel(uuid, layout);
            List<String> timeLayout = mi.getStringList(mi.getLayouts(), "Time." + layout);
            String timeName = timeLayout.get(Math.min(i, timeLayout.size() - 1));
            if (timeName.equalsIgnoreCase("perma")) {
                return new TimeCalculation(layout, -1L);
            }
            Long actualTime = TimeManager.getTime() + TimeManager.toMilliSec(timeName);
            return new TimeCalculation(layout, actualTime);
        }
        long toAdd = TimeManager.toMilliSec(time);
        if (!Universal.get().hasPerms(input.getSender(), "ab." + type.getName() + ".dur.max")) {
            long max = -1;
            for (int i = 10; i >= 1; i--) {
                if (Universal.get().hasPerms(input.getSender(), "ab." + type.getName() + ".dur." + i) &&
                        mi.contains(mi.getConfig(), "TempPerms." + i)) {
                    max = mi.getLong(mi.getConfig(), "TempPerms." + i) * 1000;
                    break;
                }
            }
            if (max != -1 && toAdd > max) {
                MessageManager.sendMessage(input.getSender(), type.getName() + ".MaxDuration", true, "MAX", max / 1000 + "");
                return null;
            }
        }
        return new TimeCalculation(null, TimeManager.getTime() + toAdd);
    }

    // Checks whether target is exempted from punishment
    private static boolean processExempt(String name, String target, Object sender, PunishmentType type) {
        MethodInterface mi = Universal.get().getMethods();
        String dataName = name.toLowerCase();

        boolean exempt = false;
        if (mi.isOnline(dataName)) {
            Object onlineTarget = mi.getPlayer(dataName);
            exempt = canNotPunish((perms) -> mi.hasPerms(sender, perms), (perms) -> mi.hasPerms(onlineTarget, perms), type.getName());
        } else {
            final Permissionable offlinePermissionPlayer = mi.getOfflinePermissionPlayer(name);
            exempt = Universal.get().isExemptPlayer(dataName) ||
                    canNotPunish((perms) -> mi.hasPerms(sender, perms), offlinePermissionPlayer::hasPermission, type.getName());
        }

        if (exempt) {
            MessageManager.sendMessage(sender, type.getBasic().getName() + ".Exempt",
                    true, "NAME", name);
            return true;
        }
        return false;
    }

    // Check based on exempt level if some is able to ban a player
    public static boolean canNotPunish(Function<String, Boolean> operatorHasPerms, Function<String, Boolean> targetHasPerms, String path) {
        final String perms = "ab." + path + ".exempt";
        if (targetHasPerms.apply(perms))
            return true;

        int targetLevel = permissionLevel(targetHasPerms, perms);

        return targetLevel != 0 && permissionLevel(operatorHasPerms, perms) <= targetLevel;
    }

    private static int permissionLevel(Function<String, Boolean> hasPerms, String permission) {
        for (int i = 10; i >= 1; i--)
            if (hasPerms.apply(permission + "." + i))
                return i;
        return 0;
    }

    // Checks whether input contains tag and removes it
    private static boolean processTag(Command.CommandInput input, String tag) {
        // Check the first few arguments for the tag
        String[] args = input.getArgs();
        for (int i = 0; i < args.length && i < 4; i++) {
            if (tag.equalsIgnoreCase(args[i])) {
                input.removeArgument(i);
                return true;
            }
        }
        return false;
    }

    private static boolean alreadyPunished(String target, PunishmentType type) {
        return (type.getBasic() == PunishmentType.MUTE && PunishmentManager.get().isMuted(target))
                || (type.getBasic() == PunishmentType.BAN && PunishmentManager.get().isBanned(target));
    }

    private static class TimeCalculation {
        private String template;
        private Long time;

        public TimeCalculation(String template, Long time) {
            this.template = template;
            this.time = time;
        }
    }
} public class RevokeByIdProcessor implements Consumer<Command.CommandInput> {
    private String path;
    private Function<Integer, Punishment> resolver;

    public RevokeByIdProcessor(String path, Function<Integer, Punishment> resolver) {
        this.path = path;
        this.resolver = resolver;
    }


    @Override
    public void accept(Command.CommandInput input) {
        int id = Integer.parseInt(input.getPrimary());

        Punishment punishment = resolver.apply(id);
        if (punishment == null) {
            MessageManager.sendMessage(input.getSender(), path + ".NotFound",
                    true, "ID", id + "");
            return;
        }

        final String operator = Universal.get().getMethods().getName(input.getSender());
        punishment.delete(operator, false, true);
        MessageManager.sendMessage(input.getSender(), path + ".Done",
                true, "ID", id + "");
    }
} public class RevokeProcessor implements Consumer<Command.CommandInput> {
    private PunishmentType type;

    public RevokeProcessor(PunishmentType type) {
        this.type = type;
    }

    @Override
    public void accept(Command.CommandInput input) {
        String name = input.getPrimary();

        String target = name;
        if(!target.matches("^(?:[0-9]{1,3}\\.){3}[0-9]{1,3}$")) {
            target = processName(input);
            if (target == null)
                return;
        }

        Punishment punishment = getPunishment(target, type);
        if (punishment == null) {
            MessageManager.sendMessage(input.getSender(), "Un" + type.getName() + ".NotPunished",
                    true, "NAME", name);
            return;
        }

        final String operator = Universal.get().getMethods().getName(input.getSender());
        punishment.delete(operator, false, true);
        MessageManager.sendMessage(input.getSender(), "Un" + type.getName() + ".Done",
                true, "NAME", name);
    }
} public class BasicTabCompleter extends CleanTabCompleter {
    public BasicTabCompleter(String... firstLayerArguments) {
        super((user, args) -> {
            if(args.length == 1){
                return MutableTabCompleter.list(firstLayerArguments);
            } else {
                return MutableTabCompleter.list();
            }
        });
    }
} public class CleanTabCompleter implements MutableTabCompleter {
    private final MutableTabCompleter rawTabCompleter;
    public static final String PLAYER_PLACEHOLDER = "PLAYERS";

    public CleanTabCompleter(MutableTabCompleter rawTabCompleter) {
        this.rawTabCompleter = rawTabCompleter;
    }

    @Override
    public ArrayList<String> onTabComplete(Object user, String[] args) {
        ArrayList<String> suggestions = rawTabCompleter.onTabComplete(user, args);

        if(!suggestions.isEmpty() && suggestions.get(0).equals(PLAYER_PLACEHOLDER)) {
            suggestions.remove(0);
            for (Object player : Universal.get().getMethods().getOnlinePlayers()){
                suggestions.add(Universal.get().getMethods().getName(player));
            }
        }

        if(args.length > 0)
            suggestions.removeIf(s -> !s.startsWith(args[args.length - 1]));

        return suggestions;
    }
} public class PunishmentTabCompleter implements TabCompleter {

    private final boolean temporary;

    public PunishmentTabCompleter(boolean temporary) {
        this.temporary = temporary;
    }

    @Override
    public List<String> onTabComplete(Object user, String[] args) {
        final MethodInterface methodInterface = Universal.get().getMethods();

        List<String> suggestions = new ArrayList<>();

        boolean hiddenTag = false;
        if(args.length > 1 && args[0].equalsIgnoreCase("-s")) {
            args = ArrayUtils.remove(args, 0);
            hiddenTag = true;
        }

        if(args.length == 1){
            if(!hiddenTag)
                suggestions.add("-s");

            for (Object player : methodInterface.getOnlinePlayers()){
                suggestions.add(methodInterface.getName(player));
            }
            suggestions.add("[Name]");
        } else if(temporary && args.length == 2){
            String current = args[args.length-1];
            String amount = current.toLowerCase().split("(?<=\\D)(?=\\d)|(?<=\\d)(?=\\D)")[0];
            if(current.equals(""))
                amount = "X";

            if(amount.matches("\\d+|X")){
                for(String unit : new String[]{"s", "m", "h", "d", "w", "mo"}){
                    suggestions.add(amount + unit);
                }
            }
            for (String layout : methodInterface.getKeys(methodInterface.getLayouts(), "Time")) {
                suggestions.add("#"+layout);
            }
        } else if((temporary && args.length == 3) || args.length == 2) {
            suggestions.add("Reason...");
            for (String layout : methodInterface.getKeys(methodInterface.getLayouts(), "Message")) {
                suggestions.add("@"+layout);
            }
        }

        if(args.length > 0){
            String[] finalArgs = args;
            suggestions.removeIf(s -> !s.startsWith(finalArgs[finalArgs.length - 1]));
        }

        return suggestions;
    }
} @FunctionalInterface
public interface TabCompleter {
    List<String> onTabComplete(Object user, String[] args);
} public class CommandUtils {
    public static Punishment getPunishment(String target, PunishmentType type) {
        return type == PunishmentType.MUTE
                ? PunishmentManager.get().getMute(target)
                : PunishmentManager.get().getBan(target);
    }

    // Removes name argument and returns uuid (null if failed)
    public static String processName(Command.CommandInput input) {
        String name = input.getPrimary();
        input.next();
        String uuid = UUIDManager.get().getUUID(name.toLowerCase());

        if (uuid == null)
            MessageManager.sendMessage(input.getSender(), "General.FailedFetch",
                    true, "NAME", name);

        return uuid;
    }

    // Removes name/ip argument and returns ip (null if failed)
    public static String processIP(Command.CommandInput input) {
        String name = input.getPrimaryData();
        input.next();
        if (name.matches("^(?:[0-9]{1,3}\\.){3}[0-9]{1,3}$")) {
            return name;
        }
		String ip = Universal.get().getIps().get(name);

		if (ip == null)
		    MessageManager.sendMessage(input.getSender(), "Ipban.IpNotCashed",
		            true, "NAME", name);

		return ip;
    }

    // Builds reason from remaining arguments (null if failed)
    public static String processReason(Command.CommandInput input) {
        MethodInterface mi = Universal.get().getMethods();
        String reason = String.join(" ", input.getArgs());

        if (reason.matches("[~@].+") && !mi.contains(mi.getLayouts(), "Message." + input.getPrimary().substring(1))) {
            MessageManager.sendMessage(input.getSender(), "General.LayoutNotFound",
                    true, "NAME", input.getPrimary().substring(1));
            return null;
        }

        return reason;
    }
} static <T> ArrayList<T> list(T... elements){
    return new ArrayList<T>(Arrays.asList(elements));
}
import me.leoko.advancedban.MethodInterface;
import me.leoko.advancedban.Universal;
import me.leoko.advancedban.manager.DatabaseManager;
import me.leoko.advancedban.manager.MessageManager;
import me.leoko.advancedban.manager.PunishmentManager;
import me.leoko.advancedban.manager.UUIDManager;
import me.leoko.advancedban.utils.commands.ListProcessor;
import me.leoko.advancedban.utils.commands.PunishmentProcessor;
import me.leoko.advancedban.utils.commands.RevokeByIdProcessor;
import me.leoko.advancedban.utils.commands.RevokeProcessor;
import me.leoko.advancedban.utils.tabcompletion.BasicTabCompleter;
import me.leoko.advancedban.utils.tabcompletion.CleanTabCompleter;
import me.leoko.advancedban.utils.tabcompletion.PunishmentTabCompleter;
import me.leoko.advancedban.utils.tabcompletion.TabCompleter;
import org.apache.commons.lang3.ArrayUtils;
import java.util.Calendar;
import java.util.GregorianCalendar;
import java.util.List;
import java.util.function.Consumer;
import java.util.function.Predicate;
import static me.leoko.advancedban.utils.CommandUtils.*;
import static me.leoko.advancedban.utils.tabcompletion.MutableTabCompleter.list;
                boolean ipCached = PunishmentManager.get().isCached(ip);
                boolean uuidCached = PunishmentManager.get().isCached(uuid);

                Object sender = input.getSender();
                MessageManager.sendMessage(sender, "Check.Header", true, "NAME", name, "CACHED", nameCached ? cached : notCached);
                MessageManager.sendMessage(sender, "Check.UUID", false, "UUID", uuid, "CACHED", uuidCached ? cached : notCached);
                if (Universal.get().hasPerms(sender, "ab.check.ip")) {
                    MessageManager.sendMessage(sender, "Check.IP", false, "IP", ip, "CACHED", ipCached ? cached : notCached);
                }
                MessageManager.sendMessage(sender, "Check.Geo", false, "LOCATION", loc == null ? "failed!" : loc);
                MessageManager.sendMessage(sender, "Check.Mute", false, "DURATION", mute == null ? "§anone" : mute.getType().isTemp() ? "§e" + mute.getDuration(false) : "§cperma");
                if (mute != null) {
                    MessageManager.sendMessage(sender, "Check.MuteReason", false, "REASON", mute.getReason());
                }
                MessageManager.sendMessage(sender, "Check.Ban", false, "DURATION", ban == null ? "§anone" : ban.getType().isTemp() ? "§e" + ban.getDuration(false) : "§cperma");
                if (ban != null) {
                    MessageManager.sendMessage(sender, "Check.BanReason", false, "REASON", ban.getReason());
                }
                MessageManager.sendMessage(sender, "Check.Warn", false, "COUNT", PunishmentManager.get().getCurrentWarns(uuid) + "");

                MessageManager.sendMessage(sender, "Check.Note", false, "COUNT", PunishmentManager.get().getCurrentNotes(uuid) + "");
            },
            "Check.Usage",
            "check"),

    SYSTEM_PREFERENCES("ab.systemprefs",
            ".*",
            null,
            input -> {
                MethodInterface mi = Universal.get().getMethods();
                Calendar calendar = new GregorianCalendar();
                Object sender = input.getSender();
                mi.sendMessage(sender, "§c§lAdvancedBan v2 §cSystemPrefs");
                mi.sendMessage(sender, "§cServer-Time §8» §7" + calendar.get(Calendar.HOUR_OF_DAY) + ":" + calendar.get(Calendar.MINUTE));
                mi.sendMessage(sender, "§cYour UUID (Intern) §8» §7" + mi.getInternUUID(sender));
                if (input.hasNext()) {
                    String target = input.getPrimaryData();
                    mi.sendMessage(sender, "§c" + target + "'s UUID (Intern) §8» §7" + mi.getInternUUID(target));
                    mi.sendMessage(sender, "§c" + target + "'s UUID (Fetched) §8» §7" + UUIDManager.get().getUUID(target));
                }
            },
            null,
            "systemprefs"),

    ADVANCED_BAN(null,
            ".*",
            new BasicTabCompleter("help", "reload"),
            input -> {
                MethodInterface mi = Universal.get().getMethods();
                Object sender = input.getSender();
                if (input.hasNext()) {
                    if (input.getPrimaryData().equals("reload")) {
                        if (Universal.get().hasPerms(sender, "ab.reload")) {
                            mi.loadFiles();
                            mi.sendMessage(sender, "§a§lAdvancedBan §8§l» §7Reloaded!");
                        } else {
                            MessageManager.sendMessage(sender, "General.NoPerms", true);
                        }
                        return;
                    } else if (input.getPrimaryData().equals("help")) {
                        if (Universal.get().hasPerms(sender, "ab.help")) {
                            mi.sendMessage(sender, "§8");
                            mi.sendMessage(sender, "§c§lAdvancedBan §7Command-Help");
                            mi.sendMessage(sender, "§8");
                            mi.sendMessage(sender, "§c/ban [Name] [Reason/@Layout]");
                            mi.sendMessage(sender, "§8» §7Ban a user permanently");
                            mi.sendMessage(sender, "§c/banip [Name/IP] [Reason/@Layout]");
                            mi.sendMessage(sender, "§8» §7Ban a user by IP");
                            mi.sendMessage(sender, "§c/tempban [Name] [Xmo/Xd/Xh/Xm/Xs/#TimeLayout] [Reason/@Layout]");
                            mi.sendMessage(sender, "§8» §7Ban a user temporary");
                            mi.sendMessage(sender, "§c/mute [Name] [Reason/@Layout]");
                            mi.sendMessage(sender, "§8» §7Mute a user permanently");
                            mi.sendMessage(sender, "§c/tempmute [Name] [Xmo/Xd/Xh/Xm/Xs/#TimeLayout] [Reason/@Layout]");
                            mi.sendMessage(sender, "§8» §7Mute a user temporary");
                            mi.sendMessage(sender, "§c/warn [Name] [Reason/@Layout]");
                            mi.sendMessage(sender, "§8» §7Warn a user permanently");
                            mi.sendMessage(sender, "§c/note [Name] [Note]");
                            mi.sendMessage(sender, "§8» §7Adds a note to a user");
                            mi.sendMessage(sender, "§c/tempwarn [Name] [Xmo/Xd/Xh/Xm/Xs/#TimeLayout] [Reason/@Layout]");
                            mi.sendMessage(sender, "§8» §7Warn a user temporary");
                            mi.sendMessage(sender, "§c/kick [Name] [Reason/@Layout]");
                            mi.sendMessage(sender, "§8» §7Kick a user");
                            mi.sendMessage(sender, "§c/unban [Name/IP]");
                            mi.sendMessage(sender, "§8» §7Unban a user");
                            mi.sendMessage(sender, "§c/unmute [Name]");
                            mi.sendMessage(sender, "§8» §7Unmute a user");
                            mi.sendMessage(sender, "§c/unwarn [ID] or /unwarn clear [Name]");
                            mi.sendMessage(sender, "§8» §7Deletes a warn");
                            mi.sendMessage(sender, "§c/unnote [ID] or /unnote clear [Name]");
                            mi.sendMessage(sender, "§8» §7Deletes a note");
                            mi.sendMessage(sender, "§c/change-reason [ID or ban/mute USER] [New reason]");
                            mi.sendMessage(sender, "§8» §7Changes the reason of a punishment");
                            mi.sendMessage(sender, "§c/unpunish [ID]");
                            mi.sendMessage(sender, "§8» §7Deletes a punishment by ID");
                            mi.sendMessage(sender, "§c/banlist <Page>");
                            mi.sendMessage(sender, "§8» §7See all punishments");
                            mi.sendMessage(sender, "§c/history [Name/IP] <Page>");
                            mi.sendMessage(sender, "§8» §7See a users history");
                            mi.sendMessage(sender, "§c/warns [Name] <Page>");
                            mi.sendMessage(sender, "§8» §7See your or a users warnings");
                            mi.sendMessage(sender, "§c/notes [Name] <Page>");
                            mi.sendMessage(sender, "§8» §7See your or a users notes");
                            mi.sendMessage(sender, "§c/check [Name]");
                            mi.sendMessage(sender, "§8» §7Get all information about a user");
                            mi.sendMessage(sender, "§c/AdvancedBan <reload/help>");
                            mi.sendMessage(sender, "§8» §7Reloads the plugin or shows help page");
                            mi.sendMessage(sender, "§8");
                        } else {
                            MessageManager.sendMessage(sender, "General.NoPerms", true);
                        }
                        return;
                    }
                }


                mi.sendMessage(sender, "§8§l§m-=====§r §c§lAdvancedBan v2 §8§l§m=====-§r ");
                mi.sendMessage(sender, "  §cDev §8• §7Leoko");
                mi.sendMessage(sender, "  §cStatus §8• §a§oStable");
                mi.sendMessage(sender, "  §cVersion §8• §7" + mi.getVersion());
                mi.sendMessage(sender, "  §cLicense §8• §7Public");
                mi.sendMessage(sender, "  §cStorage §8• §7" + (DatabaseManager.get().isUseMySQL() ? "MySQL (external)" : "HSQLDB (local)"));