public class RedisConnection {

    private PermissionManagerBase permissionManager;
    private PowerfulPermsPlugin plugin;

    private String ip;
    private int port;
    private String password;
    private int timeout = 2000;

    private JedisPool pool;
    private JedisPubSub subscriber;
    private Jedis jedis;
    private boolean isSubscribing = false;

    private int taskId;

    public RedisConnection(PermissionManagerBase permissionManager, PowerfulPermsPlugin plugin, String ip, int port, String password) {
        this.permissionManager = permissionManager;
        this.plugin = plugin;
        this.ip = ip;
        this.port = port;
        this.password = password;
        setupPool();
        setupSubscriber();
        subscribeSubscriber();
        taskId = permissionManager.getScheduler().runRepeating(() -> {
            Jedis jedis = getConnection();
            if (jedis != null)
                jedis.close();
        }, 60);
    }

    private void setupPool() {
        GenericObjectPoolConfig config = new GenericObjectPoolConfig();
        config.setTestOnBorrow(true);
        if (password == null || password.isEmpty())
            pool = new JedisPool(config, ip, port, timeout);
        else
            pool = new JedisPool(config, ip, port, timeout, password);
    }

    private void setupSubscriber() {
        subscriber = (new JedisPubSub() {
            @Override
            public void onMessage(String channel, final String msg) {
                permissionManager.getScheduler().runAsync(() -> {
                    // Reload player or groups depending on message
                    String[] split = msg.split(" ");
                    if (split.length >= 2) {
                        String first = split[0];
                        String server = split[1];

                        if (server.equals(PermissionManagerBase.serverId))
                            return;
                        if (first.equals("[groups]")) {
                            permissionManager.loadGroups();
                            plugin.getLogger().info(PermissionManagerBase.consolePrefix + "Reloaded all groups.");
                        } else if (first.equals("[players]")) {
                            permissionManager.loadGroups();
                            plugin.getLogger().info(PermissionManagerBase.consolePrefix + "Reloaded all players.");
                        } else if (first.equals("[ping]") && split.length >= 3) {
                            String sender = split[2];
                            Jedis temp = pool.getResource();
                            temp.publish("PowerfulPerms", "[pingreply]" + " " + PermissionManagerBase.serverName + " " + sender);
                            temp.close();
                        } else if (first.equals("[pingreply]") && split.length >= 3) {
                            String sender = split[2];
                            if (!plugin.isBungeeCord() || sender.equalsIgnoreCase("console"))
                                plugin.sendPlayerMessage(sender, "Received Redis ping from server \"" + server + "\".");
                        } else {
                            UUID uuid = UUID.fromString(first);
                            permissionManager.reloadPlayer(uuid);
                        }
                    }
                }, false);
            }

            @Override
            public void onSubscribe(String channel, int subscribedChannels) {
                isSubscribing = false;
            }

            @Override
            public void onUnsubscribe(String channel, int subscribedChannels) {
                isSubscribing = false;
            }
        });
    }

    private void subscribeSubscriber() {
        try {
            jedis = new Jedis(ip, port);
            if (password != null && !password.isEmpty())
                jedis.auth(password);
            isSubscribing = true;
            permissionManager.getScheduler().runAsync(() -> {
                try {
                    if (subscriber != null && subscriber.isSubscribed())
                        subscriber.unsubscribe();
                    setupSubscriber();
                    jedis.subscribe(subscriber, "PowerfulPerms");
                } catch (JedisConnectionException e) {
                    isSubscribing = false;
                    plugin.getLogger().warning("Redis connection failed: " + e.getMessage());
                } finally {
                    if (jedis != null)
                        jedis.close();
                    subscriber = null;
                    if (pool != null)
                        pool.close();
                }
            }, false);

        } catch (JedisConnectionException e) {
            isSubscribing = false;
            if (jedis != null)
                jedis.close();
            subscriber = null;
            if (pool != null)
                pool.close();
            plugin.getLogger().warning("Could not connect to your Redis server: " + e.getMessage());
        }
    }

    public Jedis getConnection() {
        if (pool == null || pool.isClosed()) {
            if (pool != null)
                pool.destroy();
            plugin.getLogger().info("Setting up Redis pool.");
            setupPool();
        }
        if (subscriber == null) {
            plugin.getLogger().info("Setting up Redis subscriber.");
            setupSubscriber();
        }
        if (subscriber != null && !subscriber.isSubscribed() && !isSubscribing)
            subscribeSubscriber();

        Jedis jedis = null;
        try {
            jedis = pool.getResource();
        } catch (JedisConnectionException e) {
            if (jedis != null)
                jedis.close();
            if (pool != null)
                pool.close();
            plugin.getLogger().warning("Could not connect to your Redis server: " + e.getMessage());
        }
        return jedis;
    }

    public void destroy() {
        if (subscriber != null)
            subscriber.unsubscribe();
        if (pool != null)
            pool.close();
        permissionManager.getScheduler().stopRepeating(taskId);
    }
} public class PowerfulEventHandler implements EventHandler {
    private List<PowerfulPermsListener> listeners;

    public PowerfulEventHandler() {
        listeners = new ArrayList<>();
    }

    public void registerListener(PowerfulPermsListener listener) {
        listeners.add(listener);
    }

    public void unregisterListener(PowerfulPermsListener listener) {
        listeners.remove(listener);
    }

    public void fireEvent(Event event) {
        for (PowerfulPermsListener listener : listeners) {
            for (Method method : listener.getClass().getDeclaredMethods()) {
                if (method.isAnnotationPresent(PowerfulEvent.class)) {
                    if (method.getParameterTypes().length > 0) {
                        Class<?> methodParameter = method.getParameterTypes()[0];
                        if (Event.class.isAssignableFrom(methodParameter)) {
                            if (event.getClass().equals(methodParameter)) {
                                try {
                                    method.invoke(listener, event);
                                } catch (IllegalAccessException | InvocationTargetException | IllegalArgumentException e) {
                                    e.printStackTrace();
                                }
                            }
                        }
                    }

                }
            }
        }
    }

} public class DBResult {

    protected ArrayList<DBDocument> rows;
    protected boolean result;
    protected int amount = 0;
    protected int index = -1;

    public DBResult(ArrayList<DBDocument> rows) {
        this.rows = rows;
        this.result = true;
    }

    public DBResult(boolean result) {
        this.result = result;
    }

    public DBResult(boolean result, int amount) {
        this.result = result;
        this.amount = amount;
    }

    public DBDocument next() {
        index++;
        if (rows.size() >= index + 1)
            return rows.get(index);
        return null;
    }

    public boolean hasNext() {
        if (rows.size() >= index + 2)
            return rows.get(index + 1) != null;
        return false;
    }
    
    public int getRows() {
        return rows.size();
    }

    public boolean booleanValue() {
        return this.result;
    }

    public int rowsChanged() {
        return this.amount;
    }

} public abstract class Database implements IDatabase {
    public IScheduler scheduler;
    public static String tblGroupParents = "groupparents";
    public static String tblGroupPermissions = "grouppermissions";
    public static String tblGroupPrefixes = "groupprefixes";
    public static String tblGroups = "groups";
    public static String tblGroupSuffixes = "groupsuffixes";
    public static String tblPlayerGroups = "playergroups";
    public static String tblPlayerPermissions = "playerpermissions";
    public static String tblPlayers = "players";
    public static String prefix = "";

    public Database(IScheduler scheduler, String prefix) {
        this.scheduler = scheduler;

        tblGroupParents = "groupparents";
        tblGroupPermissions = "grouppermissions";
        tblGroupPrefixes = "groupprefixes";
        tblGroups = "groups";
        tblGroupSuffixes = "groupsuffixes";
        tblPlayerGroups = "playergroups";
        tblPlayerPermissions = "playerpermissions";
        tblPlayers = "players";

        if (prefix != null && !prefix.isEmpty())
            setTablePrefix(prefix);
    }

    public void setTablePrefix(String prefix) {
        Database.prefix = prefix;
        tblGroupParents = prefix + tblGroupParents;
        tblGroupPermissions = prefix + tblGroupPermissions;
        tblGroupPrefixes = prefix + tblGroupPrefixes;
        tblGroups = prefix + tblGroups;
        tblGroupSuffixes = prefix + tblGroupSuffixes;
        tblPlayerGroups = prefix + tblPlayerGroups;
        tblPlayerPermissions = prefix + tblPlayerPermissions;
        tblPlayers = prefix + tblPlayers;
    }
} public class Utils {

    public static Date getDate(String date) {
        java.text.SimpleDateFormat dateFormat = new java.text.SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        try {
            return dateFormat.parse(date);
        } catch (ParseException e) {
            // Not timestamp format, try parse
            Calendar calendar = Calendar.getInstance();

            String currentNumber = "";
            String currentWord = "";
            char[] chars = date.toCharArray();
            for (char cur : chars) {
                if (Character.isDigit(cur)) {
                    if (!currentNumber.isEmpty() && !currentWord.isEmpty()) {

                        if (!finish(calendar, currentNumber, currentWord))
                            return null;
                        currentNumber = "";
                        currentWord = "";
                    }
                    currentNumber += cur;
                } else if (!currentNumber.isEmpty()) {
                    if (!Character.isSpaceChar(cur))
                        currentWord += cur;
                } else
                    return null;
            }

            if (!finish(calendar, currentNumber, currentWord))
                return null;

            calendar.add(Calendar.SECOND, 1);
            calendar.set(Calendar.MILLISECOND, 0);
            return calendar.getTime();
        }
    }

    private static boolean finish(Calendar calendar, String currentNumber, String currentWord) {
        int number = Integer.parseInt(currentNumber);

        switch (currentWord) {
            case "s":
            case "sec":
            case "second":
            case "seconds":
                calendar.add(Calendar.SECOND, number);
                break;
            case "mi":
            case "min":
            case "mins":
            case "minutes":
                calendar.add(Calendar.MINUTE, number);
                break;
            case "h":
            case "hour":
            case "hours":
                calendar.add(Calendar.HOUR, number);
                break;
            case "d":
            case "day":
            case "days":
                calendar.add(Calendar.DAY_OF_MONTH, number);
                break;
            case "w":
            case "week":
            case "weeks":
                calendar.add(Calendar.WEEK_OF_MONTH, number);
                break;
            case "m":
            case "month":
            case "months":
                calendar.add(Calendar.MONTH, number);
                break;
            case "y":
            case "year":
            case "years":
                calendar.add(Calendar.YEAR, number);
                break;
            default:
                return false;
        }
        return true;
    }

    public static String getExpirationDateString(Date expires) {
        if (expires == null)
            return null;
        java.text.SimpleDateFormat dateFormat = new java.text.SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        return dateFormat.format(expires);
    }

    public static boolean isAny(Date expires) {
        return expires != null && expires.getTime() == 0;
    }

    public static Date getAnyDate() {
        return new Date(0);
    }

    public static boolean dateApplies(Date date, Date input) {
        if (date == null && input == null)
            return true;
        else if (Utils.isAny(input))
            return true;
        else if (date == null || input == null)
            return false;
        return date.equals(input);
    }
} public class CachedGroup {
    private int id;
    private int groupId;
    private boolean negated;
    private Date expires;
    private int expireTaskId = -1;

    public CachedGroup(int id, int groupId, boolean negated, Date expires) {
        this.id = id;
        this.groupId = groupId;
        this.negated = negated;
        this.expires = expires;
    }

    public int getId() {
        return this.id;
    }

    public int getGroupId() {
        return this.groupId;
    }

    public boolean isNegated() {
        return this.negated;
    }

    public Date getExpirationDate() {
        return expires;
    }

    public boolean willExpire() {
        return expires != null;
    }

    public boolean hasExpired() {
        return willExpire() && getExpirationDate().before(new Date());
    }

    public int getExpireTaskId() {
        return expireTaskId;
    }

    public void setExpireTaskId(int taskId) {
        this.expireTaskId = taskId;
    }
} public class DBDocument {

    protected Map<String, Object> data;

    public DBDocument(Map<String, Object> data) {
        this.data = data;
    }

    public Object get(String key) {
        return data.get(key);
    }

    public String getString(String key) {
        return (String) data.get(key);
    }

    public int getInt(String key) {
        Object input = data.get(key);
        if (input instanceof Long) {
            return ((Long) input).intValue();
        }
        return (Integer) input;
    }

    public boolean getBoolean(String key) {
        return (Boolean) data.get(key);
    }

    public Timestamp getTimeStamp(String key) {
        return (Timestamp) data.get(key);
    }

    public Date getDate(String key) {
        if (getTimeStamp(key) != null) {
            Calendar start = Calendar.getInstance();
            start.setTimeInMillis(getTimeStamp(key).getTime());
            return start.getTime();
        }
        return null;
    }
} public class DefaultPermissionPlayer {

    public static UUID getUUID() {
        return java.util.UUID.nameUUIDFromBytes(("[default]").getBytes(Charsets.UTF_8));
    }

} public interface EventHandler {

    public void registerListener(PowerfulPermsListener listener);

    public void unregisterListener(PowerfulPermsListener listener);

    public void fireEvent(Event event);

} public interface Group {

    public int getId();

    public String getName();

    public List<Group> getParents();

    public String getPrefix(String server);

    public String getSuffix(String server);

    public HashMap<String, String> getPrefixes();

    public HashMap<String, String> getSuffixes();

    public List<Permission> getOwnPermissions();

    public List<Permission> getPermissions();

    public String getLadder();

    public int getRank();

    public void setParents(List<Integer> parents);

} public class GroupPermissionExpiredEvent extends Event {
    private final Group group;
    private final Permission permission;

    public GroupPermissionExpiredEvent(Group group, Permission permission) {
        this.group = group;
        this.permission = permission;
    }

    public Group getGroup() {
        return this.group;
    }

    public Permission getPermission() {
        return this.permission;
    }
} public interface IScheduler {
    public void runAsync(Runnable runnable, boolean sameThread);

    /*
     * Note: Bungee can't run sync tasks, all of them are asynchronous.
     */
    public void runSync(Runnable runnable, boolean sameThread);

    /*
     * Note: Bungee can't run sync tasks, all of them are asynchronous.
     */
    public void runSync(Runnable runnable);

    public int runRepeating(Runnable runnable, int seconds);

    public void stopRepeating(int taskId);

    public int runDelayed(Runnable runnable, Date when);

    public int runDelayed(Runnable runnable, long seconds);
} public interface Permission {

    /**
     * Returns the ID of this permission as it is stored in the database.
     */
    public int getId();

    /**
     * Returns the permission string.
     */
    public String getPermissionString();

    /**
     * Returns the name of the world the permission applies to.
     */
    public String getWorld();

    /**
     * Returns the name of the server the permission applies to. If empty or "all", applies to all servers.
     */
    public String getServer();

    /**
     * Returns the date when this permission expires. If no expiration date, it is null.
     */
    public Date getExpirationDate();

    /*
     * Returns true if this is a timed permission.
     */
    public boolean willExpire();

    /*
     * Returns true if this is a timed permission and has expired.
     */
    public boolean hasExpired();

} public interface PermissionPlayer {

    public LinkedHashMap<String, List<CachedGroup>> getCachedGroups();

    public List<CachedGroup> getCachedGroups(String server);

    public List<Group> getGroups(String server);

    public List<Group> getGroups();

    public List<Permission> getPermissions();

    public List<String> getPermissionsInEffect();

    public List<Permission> getAllPermissions();

    public Boolean hasPermission(String permission);

    public boolean isPermissionSet(String permission);

    public Group getGroup(String ladder);

    public Group getPrimaryGroup();

    public String getPrefix(String ladder);

    public String getSuffix(String ladder);

    public String getPrefix();

    public String getSuffix();

    public String getOwnPrefix();

    public String getOwnSuffix();

    public boolean isDefault();

} public class PlayerGroupExpiredEvent extends Event {
    private final UUID playerUUID;
    private final CachedGroup cachedGroup;

    public PlayerGroupExpiredEvent(UUID playerUUID, CachedGroup cachedGroup) {
        this.playerUUID = playerUUID;
        this.cachedGroup = cachedGroup;
    }

    public UUID getPlayerUUID() {
        return this.playerUUID;
    }

    public CachedGroup getCachedGroup() {
        return this.cachedGroup;
    }
} public class PlayerLoadedEvent extends Event {
    private final UUID playerUUID;
    private final String playerName;

    public PlayerLoadedEvent(UUID playerUUID, String playerName) {
        this.playerUUID = playerUUID;
        this.playerName = playerName;
    }

    public UUID getPlayerUUID() {
        return this.playerUUID;
    }

    public String getPlayerName() {
        return this.playerName;
    }
} public class PlayerPermissionExpiredEvent extends Event {
    private final UUID playerUUID;
    private final Permission permission;

    public PlayerPermissionExpiredEvent(UUID playerUUID, Permission permission) {
        this.playerUUID = playerUUID;
        this.permission = permission;
    }

    public UUID getPlayerUUID() {
        return this.playerUUID;
    }

    public Permission getPermission() {
        return this.permission;
    }
} public interface PowerfulPermsPlugin {

    public PermissionManager getPermissionManager();

    public Logger getLogger();

    public void runTaskAsynchronously(Runnable runnable);

    public void runTaskLater(Runnable runnable, int delay);

    public boolean isDebug();

    public ServerMode getServerMode();

    public boolean isPlayerOnline(UUID uuid);

    public boolean isPlayerOnline(String name);

    public UUID getPlayerUUID(String name);

    public String getPlayerName(UUID uuid);

    public Map<UUID, String> getOnlinePlayers();

    public void sendPlayerMessage(String name, String message);

    public void debug(String message);

    public int getOldVersion();

    public String getVersion();

    public void loadConfig();
    
    public boolean isBungeeCord();
} public class Response {

    protected boolean success = false;
    protected String response = "";

    public Response(boolean success, String response) {
        this.success = success;
        this.response = response;
    }

    public String getResponse() {
        return this.response;
    }

    public boolean succeeded() {
        return this.success;
    }
} public enum ServerMode {
    ONLINE,
    OFFLINE,
    MIXED
}
import com.github.gustav9797.PowerfulPerms.Redis.RedisConnection;
import com.github.gustav9797.PowerfulPerms.common.event.PowerfulEventHandler;
import com.github.gustav9797.PowerfulPerms.database.DBResult;
import com.github.gustav9797.PowerfulPerms.database.Database;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.TreeMap;
import java.util.UUID;
import java.util.Map.Entry;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.locks.ReentrantLock;
import com.github.gustav9797.PowerfulPerms.command.Utils;
import com.github.gustav9797.PowerfulPermsAPI.CachedGroup;
import com.github.gustav9797.PowerfulPermsAPI.DBDocument;
import com.github.gustav9797.PowerfulPermsAPI.DefaultPermissionPlayer;
import com.github.gustav9797.PowerfulPermsAPI.EventHandler;
import com.github.gustav9797.PowerfulPermsAPI.Group;
import com.github.gustav9797.PowerfulPermsAPI.GroupPermissionExpiredEvent;
import com.github.gustav9797.PowerfulPermsAPI.IScheduler;
import com.github.gustav9797.PowerfulPermsAPI.Permission;
import com.github.gustav9797.PowerfulPermsAPI.PermissionPlayer;
import com.github.gustav9797.PowerfulPermsAPI.PlayerGroupExpiredEvent;
import com.github.gustav9797.PowerfulPermsAPI.PlayerLoadedEvent;
import com.github.gustav9797.PowerfulPermsAPI.PlayerPermissionExpiredEvent;
import com.github.gustav9797.PowerfulPermsAPI.PowerfulPermsPlugin;
import com.github.gustav9797.PowerfulPermsAPI.Response;
import com.github.gustav9797.PowerfulPermsAPI.ServerMode;
import com.google.common.base.Charsets;
import redis.clients.jedis.Jedis;
            }
            return permissions;
        } else
            plugin.getLogger().severe("Could not load group permissions.");
        return null;
    }

    public String getPlayerPrefixBase(final UUID uuid, final String ladder) {
        // If player is online, get data directly from player
        PermissionPlayer gp = getPermissionPlayer(uuid);
        if (gp != null) {
            if (ladder != null)
                return gp.getPrefix(ladder);
            return gp.getPrefix();
        }

        LinkedHashMap<String, List<CachedGroup>> currentGroups = getPlayerCurrentGroupsBase(uuid);
        List<CachedGroup> cachedGroups = PermissionPlayerBase.getCachedGroups(serverName, currentGroups);
        List<Group> groups = PermissionPlayerBase.getGroups(cachedGroups, plugin);
        String prefix = "";
        if (ladder != null)
            prefix = PermissionPlayerBase.getPrefix(ladder, groups);
        else
            prefix = PermissionPlayerBase.getPrefix(groups, getPlayerOwnPrefixBase(uuid));
        return prefix;
    }

    public String getPlayerSuffixBase(final UUID uuid, final String ladder) {
        // If player is online, get data directly from player
        PermissionPlayer gp = getPermissionPlayer(uuid);
        if (gp != null) {
            if (ladder != null)
                return gp.getSuffix(ladder);
            return gp.getSuffix();
        }

        LinkedHashMap<String, List<CachedGroup>> currentGroups = getPlayerCurrentGroupsBase(uuid);
        List<CachedGroup> cachedGroups = PermissionPlayerBase.getCachedGroups(serverName, currentGroups);
        List<Group> groups = PermissionPlayerBase.getGroups(cachedGroups, plugin);
        String suffix = "";
        if (ladder != null)
            suffix = PermissionPlayerBase.getSuffix(ladder, groups);
        else
            suffix = PermissionPlayerBase.getSuffix(groups, getPlayerOwnSuffixBase(uuid));
        return suffix;
    }

    public String getPlayerOwnPrefixBase(final UUID uuid) {
        // If player is online, get data directly from player
        PermissionPlayer gp = getPermissionPlayer(uuid);
        if (gp != null)
            return gp.getOwnPrefix();

        DBResult result = db.getPlayer(uuid);
        if (result.hasNext()) {
            DBDocument row = result.next();
            return row.getString("prefix");
        }
        return null;
    }

    public String getPlayerOwnSuffixBase(final UUID uuid) {
        // If player is online, get data directly from player
        PermissionPlayer gp = getPermissionPlayer(uuid);
        if (gp != null)
            return gp.getOwnSuffix();

        DBResult result = db.getPlayer(uuid);
        if (result.hasNext()) {
            DBDocument row = result.next();
            return row.getString("suffix");
        }
        return null;
    }

    public String getGroupPrefix(int groupId, String server) {
        Group g = getGroup(groupId);
        if (g != null)
            return g.getPrefix(server);
        return null;
    }

    public String getGroupSuffix(int groupId, String server) {
        Group g = getGroup(groupId);
        if (g != null)
            return g.getSuffix(server);
        return null;
    }

    public HashMap<String, String> getGroupServerPrefix(int groupId) {
        Group g = getGroup(groupId);
        if (g != null)
            return g.getPrefixes();
        return null;
    }

    public HashMap<String, String> getGroupServerSuffix(int groupId) {
        Group g = getGroup(groupId);
        if (g != null)
            return g.getSuffixes();
        return null;
    }

    public UUID getConvertUUIDBase(final String playerName) {
        if (playerName.equalsIgnoreCase("[default]") || playerName.equalsIgnoreCase("{default}"))
            return DefaultPermissionPlayer.getUUID();

        // If player name is UUID, return it directly
        try {
            UUID uuid = UUID.fromString(playerName);
            return uuid;
        } catch (Exception e) {
        }

        // If player is online, get UUID directly
        if (plugin.isPlayerOnline(playerName))
            return plugin.getPlayerUUID(playerName);

        // If player UUID exists in database, use that
        DBResult result;
        if (plugin.getServerMode() == ServerMode.ONLINE)